Skip to content
Open
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
5 changes: 5 additions & 0 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1783,6 +1783,7 @@
"noFlux2KleinQwen3EncoderModelSelected": "No Qwen3 Encoder selected. Non-diffusers FLUX.2 Klein models require a standalone Qwen3 Encoder",
"noQwenImageComponentSourceSelected": "GGUF Qwen Image models require a Diffusers Component Source for VAE/encoder",
"noWanComponentSourceSelected": "GGUF Wan 2.2 models require a Diffusers Component Source for VAE/encoder",
"minimaxH3VideoOnGenerateTab": "MiniMax H3 video generation runs on the Generate tab (switch Output to Image to use MiniMax H3 on canvas)",
"noZImageVaeSourceSelected": "No VAE source: Select VAE (FLUX) or Qwen3 Source model",
"noZImageQwen3EncoderSourceSelected": "No Qwen3 Encoder source: Select Qwen3 Encoder or Qwen3 Source model",
"noKrea2VaeModelSelected": "Non-diffusers Krea-2: select a VAE in Advanced settings",
Expand Down Expand Up @@ -1846,6 +1847,10 @@
"shift": "Shift",
"shuffle": "Shuffle Seed",
"wanGuidanceScaleLowNoise": "CFG (Low)",
"minimaxH3DurationSeconds": "Duration (seconds)",
"minimaxH3OutputMode": "Output",
"minimaxH3OutputModeVideo": "Video + Audio",
"minimaxH3OutputModeImage": "Image",
"steps": "Steps",
"strength": "Strength",
"symmetry": "Symmetry",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
getEntityIdentifier,
isAspectRatioID,
isFlux2ReferenceImageConfig,
isMiniMaxH3ReferenceImageConfig,
isQwenImageReferenceImageConfig,
isWanReferenceImageConfig,
} from 'features/controlLayers/store/types';
Expand All @@ -48,6 +49,7 @@ import {
initialFluxKontextReferenceImage,
initialFLUXRedux,
initialIPAdapter,
initialMiniMaxH3ReferenceImage,
initialQwenImageReferenceImage,
initialWanReferenceImage,
} from 'features/controlLayers/store/util';
Expand Down Expand Up @@ -488,6 +490,21 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
continue;
}

if (newBase === 'minimax-h3') {
// Switching TO MiniMax H3 - convert any non-H3 configs to minimax_h3_reference_image.
// The H3 graph builder consumes the first enabled ref image as the video's first frame.
if (!isMiniMaxH3ReferenceImageConfig(entity.config)) {
dispatch(
refImageConfigChanged({
id: entity.id,
config: { ...initialMiniMaxH3ReferenceImage },
})
);
modelsUpdatedDisabledOrCleared += 1;
}
continue;
}

if (isFlux2ReferenceImageConfig(entity.config)) {
// Switching AWAY from FLUX.2 - convert flux2_reference_image to the appropriate config type
let newConfig;
Expand Down Expand Up @@ -536,6 +553,29 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
continue;
}

if (isMiniMaxH3ReferenceImageConfig(entity.config)) {
// Switching AWAY from MiniMax H3 - convert to the appropriate config type for the new base.
let newConfig;
if (newGlobalRefImageModel) {
const parsedModel = zModelIdentifierField.parse(newGlobalRefImageModel);
if (newModel.base === 'flux' && newModel.name.toLowerCase().includes('kontext')) {
newConfig = { ...initialFluxKontextReferenceImage, model: parsedModel };
} else if (newGlobalRefImageModel.type === 'flux_redux') {
newConfig = { ...initialFLUXRedux, model: parsedModel };
} else {
newConfig = { ...initialIPAdapter, model: parsedModel };
if (parsedModel.base === 'flux') {
newConfig.clipVisionModel = 'ViT-L';
}
}
} else {
newConfig = { ...initialIPAdapter };
}
dispatch(refImageConfigChanged({ id: entity.id, config: newConfig }));
modelsUpdatedDisabledOrCleared += 1;
continue;
}

if (isWanReferenceImageConfig(entity.config)) {
// Switching AWAY from Wan - convert to the appropriate config type for the new base.
let newConfig;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
isFlux2ReferenceImageConfig,
isFLUXReduxConfig,
isIPAdapterConfig,
isMiniMaxH3ReferenceImageConfig,
isQwenImageReferenceImageConfig,
isWanReferenceImageConfig,
} from 'features/controlLayers/store/types';
Expand Down Expand Up @@ -130,11 +131,12 @@ const RefImageSettingsContent = memo(() => {
const isFLUX = useAppSelector(selectIsFLUX);
const isExternalModel = !!mainModelConfig && isExternalApiModelConfig(mainModelConfig);

// FLUX.2 Klein, Qwen Image Edit, Wan 2.2 and external API models do not require a ref image model selection.
// FLUX.2 Klein, Qwen Image Edit, Wan 2.2, MiniMax H3 and external API models do not require a ref image model selection.
const showModelSelector =
!isFlux2ReferenceImageConfig(config) &&
!isQwenImageReferenceImageConfig(config) &&
!isWanReferenceImageConfig(config) &&
!isMiniMaxH3ReferenceImageConfig(config) &&
!isExternalModel;

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type {
Flux2ReferenceImageConfig,
FluxKontextReferenceImageConfig,
IPAdapterConfig,
MiniMaxH3ReferenceImageConfig,
QwenImageReferenceImageConfig,
RegionalGuidanceIPAdapterConfig,
T2IAdapterConfig,
Expand All @@ -39,6 +40,7 @@ import {
initialFlux2ReferenceImage,
initialFluxKontextReferenceImage,
initialIPAdapter,
initialMiniMaxH3ReferenceImage,
initialQwenImageReferenceImage,
initialRegionalGuidanceIPAdapter,
initialT2IAdapter,
Expand Down Expand Up @@ -87,7 +89,8 @@ export const getDefaultRefImageConfig = (
| FluxKontextReferenceImageConfig
| Flux2ReferenceImageConfig
| QwenImageReferenceImageConfig
| WanReferenceImageConfig => {
| WanReferenceImageConfig
| MiniMaxH3ReferenceImageConfig => {
const state = getState();

const mainModelConfig = selectMainModelConfig(state);
Expand All @@ -110,6 +113,11 @@ export const getDefaultRefImageConfig = (
return deepClone(initialWanReferenceImage);
}

// MiniMax H3 first-frame conditioning uses the main model's own VAE + vision context
if (base === 'minimax-h3') {
return deepClone(initialMiniMaxH3ReferenceImage);
}

if (base === 'flux' && mainModelConfig?.name?.toLowerCase().includes('kontext')) {
const config = deepClone(initialFluxKontextReferenceImage);
config.model = zModelIdentifierField.parse(mainModelConfig);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,14 @@ describe('paramsSliceConfig persisted state migration', () => {
delete v2State.hiDiffusionWindowAttnEnabled;
delete v2State.hiDiffusionT1Ratio;
delete v2State.hiDiffusionT2Ratio;
delete v2State.minimaxH3DurationSeconds;
delete v2State.minimaxH3OutputMode;

const result = migrate?.(v2State) as ReturnType<typeof getInitialParamsState>;

// v2 migrates all the way through the current chain (v2 -> v3 adds Qwen fields,
// v3 -> v4 adds Krea-2 and PiD fields).
expect(result._version).toBe(4);
// v3 -> v4 adds Krea-2 and PiD fields, v4 -> v5 adds MiniMax H3 fields).
expect(result._version).toBe(5);
expect(result.qwenImageVaeModel).toBeNull();
expect(result.qwenImageQwenVLEncoderModel).toBeNull();
expect(result.hiDiffusionEnabled).toBe(false);
Expand Down Expand Up @@ -204,10 +206,12 @@ describe('paramsSliceConfig persisted state migration', () => {
delete v3State.krea2RebalanceEnabled;
delete v3State.krea2RebalanceMultiplier;
delete v3State.krea2RebalanceWeights;
delete v3State.minimaxH3DurationSeconds;
delete v3State.minimaxH3OutputMode;

const result = migrate?.(v3State) as ReturnType<typeof getInitialParamsState>;

expect(result._version).toBe(4);
expect(result._version).toBe(5);
expect(result.krea2VaeModel).toBeNull();
expect(result.krea2Qwen3VlEncoderModel).toBeNull();
expect(result.krea2SeedVarianceEnabled).toBe(false);
Expand All @@ -221,6 +225,30 @@ describe('paramsSliceConfig persisted state migration', () => {
expect(result.dimensions).toMatchObject({ width: 640, height: 896 });
});

it('backfills the MiniMax H3 fields when migrating from v4 and preserves existing params', () => {
expect(migrate).toBeDefined();

const initial = getInitialParamsState();
const v4State: Record<string, unknown> = {
...initial,
_version: 4,
positivePrompt: 'preserve this prompt',
seed: 4242,
dimensions: { ...initial.dimensions, width: 1344, height: 768 },
};
delete v4State.minimaxH3DurationSeconds;
delete v4State.minimaxH3OutputMode;

const result = migrate?.(v4State) as ReturnType<typeof getInitialParamsState>;

expect(result._version).toBe(5);
expect(result.minimaxH3DurationSeconds).toBe(5);
expect(result.minimaxH3OutputMode).toBe('video');
expect(result.positivePrompt).toBe('preserve this prompt');
expect(result.seed).toBe(4242);
expect(result.dimensions).toMatchObject({ width: 1344, height: 768 });
});

it('backfills the ERNIE-Image fields from their zod defaults without a version bump', () => {
// The ERNIE-Image fields are additive with `.default()`, so there is no migration branch for
// them. A persisted state written before they existed must still parse -- if it throws, the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,20 @@ const slice = createSlice({
wanGuidanceScaleLowNoiseChanged: (state, action: PayloadAction<number | null>) => {
state.wanGuidanceScaleLowNoise = action.payload;
},
minimaxH3DurationSecondsChanged: (state, action: PayloadAction<number>) => {
const result = zParamsState.shape.minimaxH3DurationSeconds.safeParse(action.payload);
if (!result.success) {
return;
}
state.minimaxH3DurationSeconds = result.data;
},
minimaxH3OutputModeChanged: (state, action: PayloadAction<'video' | 'image'>) => {
const result = zParamsState.shape.minimaxH3OutputMode.safeParse(action.payload);
if (!result.success) {
return;
}
state.minimaxH3OutputMode = result.data;
},
vaePrecisionChanged: (state, action: PayloadAction<ParameterPrecision>) => {
state.vaePrecision = action.payload;
},
Expand Down Expand Up @@ -914,6 +928,8 @@ export const {
wanVaeModelSelected,
wanT5EncoderModelSelected,
wanGuidanceScaleLowNoiseChanged,
minimaxH3DurationSecondsChanged,
minimaxH3OutputModeChanged,
setClipSkip,
shouldUseCpuNoiseChanged,
setColorCompensation,
Expand Down Expand Up @@ -1005,6 +1021,13 @@ export const paramsSliceConfig: SliceConfig<typeof slice> = {
state.pidSteps = 4;
}

if (state._version === 4) {
// v4 -> v5, add the MiniMax H3 duration and output-mode fields
state._version = 5;
state.minimaxH3DurationSeconds = 5;
state.minimaxH3OutputMode = 'video';
}

if (!('hiDiffusionEnabled' in state)) {
state.hiDiffusionEnabled = false;
}
Expand Down Expand Up @@ -1043,6 +1066,7 @@ export const selectIsExternal = createParamsSelector((params) => params.model?.b
export const selectIsQwenImage = createParamsSelector((params) => params.model?.base === 'qwen-image');
export const selectIsKrea2 = createParamsSelector((params) => params.model?.base === 'krea-2');
export const selectIsWan = createParamsSelector((params) => params.model?.base === 'wan');
export const selectIsMiniMaxH3 = createParamsSelector((params) => params.model?.base === 'minimax-h3');
export const selectIsFluxKontext = createParamsSelector((params) => {
if (params.model?.base === 'flux' && params.model?.name.toLowerCase().includes('kontext')) {
return true;
Expand Down Expand Up @@ -1086,6 +1110,8 @@ export const selectWanComponentSource = createParamsSelector((params) => params.
export const selectWanVaeModel = createParamsSelector((params) => params.wanVaeModel);
export const selectWanT5EncoderModel = createParamsSelector((params) => params.wanT5EncoderModel);
export const selectWanGuidanceScaleLowNoise = createParamsSelector((params) => params.wanGuidanceScaleLowNoise);
export const selectMiniMaxH3DurationSeconds = createParamsSelector((params) => params.minimaxH3DurationSeconds);
export const selectMiniMaxH3OutputMode = createParamsSelector((params) => params.minimaxH3OutputMode);

export const selectCFGScale = createParamsSelector((params) => params.cfgScale);
export const selectGuidance = createParamsSelector((params) => params.guidance);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
isFlux2ReferenceImageConfig,
isFLUXReduxConfig,
isIPAdapterConfig,
isMiniMaxH3ReferenceImageConfig,
isQwenImageReferenceImageConfig,
isWanReferenceImageConfig,
zRefImagesState,
Expand Down Expand Up @@ -145,11 +146,12 @@ const slice = createSlice({
return;
}

// FLUX.2, Qwen Image Edit and Wan reference images don't have a model field - they use built-in support
// FLUX.2, Qwen Image Edit, Wan and MiniMax H3 reference images don't have a model field - they use built-in support
if (
isFlux2ReferenceImageConfig(entity.config) ||
isQwenImageReferenceImageConfig(entity.config) ||
isWanReferenceImageConfig(entity.config)
isWanReferenceImageConfig(entity.config) ||
isMiniMaxH3ReferenceImageConfig(entity.config)
) {
return;
}
Expand Down
24 changes: 22 additions & 2 deletions invokeai/frontend/web/src/features/controlLayers/store/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,15 @@ const zWanReferenceImageConfig = z.object({
});
export type WanReferenceImageConfig = z.infer<typeof zWanReferenceImageConfig>;

// MiniMax H3 first-frame conditioning uses the model's own VAE + vision
// context - no separate adapter model needed. Consumed only in video output
// mode (the first enabled ref image becomes the video's first frame).
const zMiniMaxH3ReferenceImageConfig = z.object({
type: z.literal('minimax_h3_reference_image'),
image: zCroppableImageWithDims.nullable(),
});
export type MiniMaxH3ReferenceImageConfig = z.infer<typeof zMiniMaxH3ReferenceImageConfig>;

const zCanvasEntityBase = z.object({
id: zId,
name: zName,
Expand All @@ -455,6 +464,7 @@ export const zRefImageState = z.object({
zFlux2ReferenceImageConfig,
zQwenImageReferenceImageConfig,
zWanReferenceImageConfig,
zMiniMaxH3ReferenceImageConfig,
]),
});
export type RefImageState = z.infer<typeof zRefImageState>;
Expand All @@ -479,6 +489,10 @@ export const isQwenImageReferenceImageConfig = (
export const isWanReferenceImageConfig = (config: RefImageState['config']): config is WanReferenceImageConfig =>
config.type === 'wan_reference_image';

export const isMiniMaxH3ReferenceImageConfig = (
config: RefImageState['config']
): config is MiniMaxH3ReferenceImageConfig => config.type === 'minimax_h3_reference_image';

const zFillStyle = z.enum(['solid', 'grid', 'crosshatch', 'diagonal', 'horizontal', 'vertical']);
export type FillStyle = z.infer<typeof zFillStyle>;
export const isFillStyle = (v: unknown): v is FillStyle => zFillStyle.safeParse(v).success;
Expand Down Expand Up @@ -817,7 +831,7 @@ const zPidMode = z.enum(['off', 'fit', 'native']);
export type PidMode = z.infer<typeof zPidMode>;

export const zParamsState = z.object({
_version: z.literal(4),
_version: z.literal(5),
maskBlur: z.number(),
maskBlurMethod: zParameterMaskBlurMethod,
canvasCoherenceMode: zParameterCanvasCoherenceMode,
Expand Down Expand Up @@ -921,6 +935,10 @@ export const zParamsState = z.object({
wanVaeModel: zParameterVAEModel.nullable(), // Optional: Standalone Wan VAE checkpoint
wanT5EncoderModel: zModelIdentifierField.nullable(), // Optional: Standalone UMT5-XXL encoder
wanGuidanceScaleLowNoise: z.number().nullable(), // Optional: separate CFG for low-noise expert (A14B). null = same as primary
// MiniMax H3 joint audio-video generation (fixed 24 fps; frame counts snap to the 17n+5 grid,
// so the effective duration ceiling is 345 frames = 14.375 s).
minimaxH3DurationSeconds: z.number().int().min(5).max(14),
minimaxH3OutputMode: z.enum(['video', 'image']),
// Z-Image Seed Variance Enhancer settings
zImageSeedVarianceEnabled: z.boolean(),
zImageSeedVarianceStrength: z.number().min(0).max(2),
Expand Down Expand Up @@ -951,7 +969,7 @@ export const zParamsState = z.object({
});
export type ParamsState = z.infer<typeof zParamsState>;
export const getInitialParamsState = (): ParamsState => ({
_version: 4,
_version: 5,
maskBlur: 16,
maskBlurMethod: 'box',
canvasCoherenceMode: 'Gaussian Blur',
Expand Down Expand Up @@ -1039,6 +1057,8 @@ export const getInitialParamsState = (): ParamsState => ({
wanVaeModel: null,
wanT5EncoderModel: null,
wanGuidanceScaleLowNoise: null,
minimaxH3DurationSeconds: 5,
minimaxH3OutputMode: 'video',
zImageSeedVarianceEnabled: false,
zImageSeedVarianceStrength: 0.1,
zImageSeedVarianceRandomizePercent: 50,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
FLUXReduxConfig,
ImageWithDims,
IPAdapterConfig,
MiniMaxH3ReferenceImageConfig,
QwenImageReferenceImageConfig,
RasterLayerAdjustments,
RefImageState,
Expand Down Expand Up @@ -128,6 +129,10 @@ export const initialWanReferenceImage: WanReferenceImageConfig = {
type: 'wan_reference_image',
image: null,
};
export const initialMiniMaxH3ReferenceImage: MiniMaxH3ReferenceImageConfig = {
type: 'minimax_h3_reference_image',
image: null,
};
export const initialT2IAdapter: T2IAdapterConfig = {
type: 't2i_adapter',
model: null,
Expand Down
Loading
Loading