From 162eb3bdb309c3fe5aea645e6b0f057395d3fa29 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Tue, 4 Aug 2026 11:53:52 -0400 Subject: [PATCH] feat(minimax-h3): linear UI (generate-tab t2v/i2v and image output mode) Co-Authored-By: Claude Fable 5 --- invokeai/frontend/web/public/locales/en.json | 5 + .../listeners/modelSelected.ts | 40 +++ .../components/RefImage/RefImageSettings.tsx | 4 +- .../controlLayers/hooks/addLayerHooks.ts | 10 +- .../controlLayers/store/paramsSlice.test.ts | 34 ++- .../controlLayers/store/paramsSlice.ts | 26 ++ .../controlLayers/store/refImagesSlice.ts | 6 +- .../src/features/controlLayers/store/types.ts | 24 +- .../src/features/controlLayers/store/util.ts | 5 + .../controlLayers/store/validators.ts | 18 +- .../web/src/features/metadata/parsing.tsx | 47 ++++ .../web/src/features/modelManagerV2/models.ts | 10 +- .../util/graph/generation/addImageToImage.ts | 3 +- .../util/graph/generation/addTextToImage.ts | 9 +- .../graph/generation/buildMiniMaxH3Graph.ts | 246 ++++++++++++++++++ .../src/features/nodes/util/graph/types.ts | 17 +- .../Core/ParamMiniMaxH3Duration.tsx | 66 +++++ .../Core/ParamMiniMaxH3OutputMode.tsx | 34 +++ .../parameters/util/optimalDimension.ts | 8 + .../features/queue/hooks/useEnqueueCanvas.ts | 3 + .../queue/hooks/useEnqueueGenerate.ts | 3 + .../web/src/features/queue/store/readiness.ts | 6 + .../GenerationSettingsAccordion.tsx | 8 +- .../generationSettingsVisibility.ts | 1 + 24 files changed, 607 insertions(+), 26 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/generation/buildMiniMaxH3Graph.ts create mode 100644 invokeai/frontend/web/src/features/parameters/components/Core/ParamMiniMaxH3Duration.tsx create mode 100644 invokeai/frontend/web/src/features/parameters/components/Core/ParamMiniMaxH3OutputMode.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index a4c7986b10c..aeb5f65fc52 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -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", @@ -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", diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts index 7acb6b22fa0..0eb534ba7fc 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts @@ -40,6 +40,7 @@ import { getEntityIdentifier, isAspectRatioID, isFlux2ReferenceImageConfig, + isMiniMaxH3ReferenceImageConfig, isQwenImageReferenceImageConfig, isWanReferenceImageConfig, } from 'features/controlLayers/store/types'; @@ -48,6 +49,7 @@ import { initialFluxKontextReferenceImage, initialFLUXRedux, initialIPAdapter, + initialMiniMaxH3ReferenceImage, initialQwenImageReferenceImage, initialWanReferenceImage, } from 'features/controlLayers/store/util'; @@ -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; @@ -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; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageSettings.tsx index 3edf9594b79..fb415d8ae45 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageSettings.tsx @@ -38,6 +38,7 @@ import { isFlux2ReferenceImageConfig, isFLUXReduxConfig, isIPAdapterConfig, + isMiniMaxH3ReferenceImageConfig, isQwenImageReferenceImageConfig, isWanReferenceImageConfig, } from 'features/controlLayers/store/types'; @@ -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 ( diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index 10603191df6..77a21fb4750 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -29,6 +29,7 @@ import type { Flux2ReferenceImageConfig, FluxKontextReferenceImageConfig, IPAdapterConfig, + MiniMaxH3ReferenceImageConfig, QwenImageReferenceImageConfig, RegionalGuidanceIPAdapterConfig, T2IAdapterConfig, @@ -39,6 +40,7 @@ import { initialFlux2ReferenceImage, initialFluxKontextReferenceImage, initialIPAdapter, + initialMiniMaxH3ReferenceImage, initialQwenImageReferenceImage, initialRegionalGuidanceIPAdapter, initialT2IAdapter, @@ -87,7 +89,8 @@ export const getDefaultRefImageConfig = ( | FluxKontextReferenceImageConfig | Flux2ReferenceImageConfig | QwenImageReferenceImageConfig - | WanReferenceImageConfig => { + | WanReferenceImageConfig + | MiniMaxH3ReferenceImageConfig => { const state = getState(); const mainModelConfig = selectMainModelConfig(state); @@ -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); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.test.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.test.ts index ff143b562b9..39387191c64 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.test.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.test.ts @@ -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; // 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); @@ -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; - expect(result._version).toBe(4); + expect(result._version).toBe(5); expect(result.krea2VaeModel).toBeNull(); expect(result.krea2Qwen3VlEncoderModel).toBeNull(); expect(result.krea2SeedVarianceEnabled).toBe(false); @@ -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 = { + ...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; + + 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 diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts index 7884020abc6..a005d980ba2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts @@ -483,6 +483,20 @@ const slice = createSlice({ wanGuidanceScaleLowNoiseChanged: (state, action: PayloadAction) => { state.wanGuidanceScaleLowNoise = action.payload; }, + minimaxH3DurationSecondsChanged: (state, action: PayloadAction) => { + 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) => { state.vaePrecision = action.payload; }, @@ -914,6 +928,8 @@ export const { wanVaeModelSelected, wanT5EncoderModelSelected, wanGuidanceScaleLowNoiseChanged, + minimaxH3DurationSecondsChanged, + minimaxH3OutputModeChanged, setClipSkip, shouldUseCpuNoiseChanged, setColorCompensation, @@ -1005,6 +1021,13 @@ export const paramsSliceConfig: SliceConfig = { 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; } @@ -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; @@ -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); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts index 6c364e51e88..913bbcd4e75 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts @@ -22,6 +22,7 @@ import { isFlux2ReferenceImageConfig, isFLUXReduxConfig, isIPAdapterConfig, + isMiniMaxH3ReferenceImageConfig, isQwenImageReferenceImageConfig, isWanReferenceImageConfig, zRefImagesState, @@ -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; } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 485b66fb25d..ce5aa6df096 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -438,6 +438,15 @@ const zWanReferenceImageConfig = z.object({ }); export type WanReferenceImageConfig = z.infer; +// 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; + const zCanvasEntityBase = z.object({ id: zId, name: zName, @@ -455,6 +464,7 @@ export const zRefImageState = z.object({ zFlux2ReferenceImageConfig, zQwenImageReferenceImageConfig, zWanReferenceImageConfig, + zMiniMaxH3ReferenceImageConfig, ]), }); export type RefImageState = z.infer; @@ -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; export const isFillStyle = (v: unknown): v is FillStyle => zFillStyle.safeParse(v).success; @@ -817,7 +831,7 @@ const zPidMode = z.enum(['off', 'fit', 'native']); export type PidMode = z.infer; export const zParamsState = z.object({ - _version: z.literal(4), + _version: z.literal(5), maskBlur: z.number(), maskBlurMethod: zParameterMaskBlurMethod, canvasCoherenceMode: zParameterCanvasCoherenceMode, @@ -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), @@ -951,7 +969,7 @@ export const zParamsState = z.object({ }); export type ParamsState = z.infer; export const getInitialParamsState = (): ParamsState => ({ - _version: 4, + _version: 5, maskBlur: 16, maskBlurMethod: 'box', canvasCoherenceMode: 'Gaussian Blur', @@ -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, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/util.ts b/invokeai/frontend/web/src/features/controlLayers/store/util.ts index a0dae2145d0..c33d3d70787 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/util.ts @@ -16,6 +16,7 @@ import type { FLUXReduxConfig, ImageWithDims, IPAdapterConfig, + MiniMaxH3ReferenceImageConfig, QwenImageReferenceImageConfig, RasterLayerAdjustments, RefImageState, @@ -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, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/validators.ts b/invokeai/frontend/web/src/features/controlLayers/store/validators.ts index 80da5a9cc5c..c84015d3e20 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/validators.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/validators.ts @@ -194,11 +194,12 @@ export const getGlobalReferenceImageWarnings = ( const { config } = entity; - // FLUX.2, Qwen Image Edit and Wan reference images don't require a model - it's built-in + // FLUX.2, Qwen Image Edit, Wan and MiniMax H3 reference images don't require a model - it's built-in if ( config.type !== 'flux2_reference_image' && config.type !== 'qwen_image_reference_image' && - config.type !== 'wan_reference_image' + config.type !== 'wan_reference_image' && + config.type !== 'minimax_h3_reference_image' ) { if (!('model' in config) || !config.model) { // No model selected @@ -210,10 +211,15 @@ export const getGlobalReferenceImageWarnings = ( } if (!entity.config.image) { - // No image selected - for Qwen Image Edit and Wan, an image is optional at the - // entity level. Wan I2V *requires* one but enforcement happens at graph-build - // time so the warning doesn't fire on T2V/TI2V variants that ignore ref images. - if (config.type !== 'qwen_image_reference_image' && config.type !== 'wan_reference_image') { + // No image selected - for Qwen Image Edit, Wan and MiniMax H3, an image is optional + // at the entity level. Wan I2V *requires* one but enforcement happens at graph-build + // time so the warning doesn't fire on T2V/TI2V variants that ignore ref images. For + // MiniMax H3 an empty ref image simply means plain text-to-video. + if ( + config.type !== 'qwen_image_reference_image' && + config.type !== 'wan_reference_image' && + config.type !== 'minimax_h3_reference_image' + ) { warnings.push(WARNINGS.IP_ADAPTER_NO_IMAGE_SELECTED); } } diff --git a/invokeai/frontend/web/src/features/metadata/parsing.tsx b/invokeai/frontend/web/src/features/metadata/parsing.tsx index c3be73c453f..52bc2eedcbe 100644 --- a/invokeai/frontend/web/src/features/metadata/parsing.tsx +++ b/invokeai/frontend/web/src/features/metadata/parsing.tsx @@ -19,6 +19,8 @@ import { kleinVaeModelSelected, krea2Qwen3VlEncoderModelSelected, krea2VaeModelSelected, + minimaxH3DurationSecondsChanged, + minimaxH3OutputModeChanged, negativePromptChanged, openaiBackgroundChanged, openaiInputFidelityChanged, @@ -1108,6 +1110,49 @@ const WanGuidanceScaleLowNoise: SingleMetadataHandler = { }; //#endregion WanGuidanceScaleLowNoise +//#region MiniMaxH3DurationSeconds +const MiniMaxH3DurationSeconds: SingleMetadataHandler = { + [SingleMetadataKey]: true, + type: 'MiniMaxH3DurationSeconds', + parse: (metadata, _store) => { + const raw = getProperty(metadata, 'minimax_h3_duration_seconds'); + if (raw === undefined) { + // Reject when the key is absent so the handler is not rendered for non-H3 media. + return Promise.reject(); + } + const parsed = z.number().int().min(5).max(14).parse(raw); + return Promise.resolve(parsed); + }, + recall: (value, store) => { + store.dispatch(minimaxH3DurationSecondsChanged(value)); + }, + i18nKey: 'parameters.minimaxH3DurationSeconds', + LabelComponent: MetadataLabel, + ValueComponent: ({ value }: SingleMetadataValueProps) => , +}; +//#endregion MiniMaxH3DurationSeconds + +//#region MiniMaxH3OutputMode +const MiniMaxH3OutputMode: SingleMetadataHandler<'video' | 'image'> = { + [SingleMetadataKey]: true, + type: 'MiniMaxH3OutputMode', + parse: (metadata, _store) => { + const raw = getProperty(metadata, 'minimax_h3_output_mode'); + if (raw === undefined) { + return Promise.reject(); + } + const parsed = z.enum(['video', 'image']).parse(raw); + return Promise.resolve(parsed); + }, + recall: (value, store) => { + store.dispatch(minimaxH3OutputModeChanged(value)); + }, + i18nKey: 'parameters.minimaxH3OutputMode', + LabelComponent: MetadataLabel, + ValueComponent: ({ value }: SingleMetadataValueProps<'video' | 'image'>) => , +}; +//#endregion MiniMaxH3OutputMode + //#region ZImageShift const ZImageShift: SingleMetadataHandler = { [SingleMetadataKey]: true, @@ -2293,6 +2338,8 @@ export const ImageMetadataHandlers = { WanVaeModel, WanT5EncoderModel, WanGuidanceScaleLowNoise, + MiniMaxH3DurationSeconds, + MiniMaxH3OutputMode, ZImageShift, Ideogram4SamplerPreset, Ideogram4Steps, diff --git a/invokeai/frontend/web/src/features/modelManagerV2/models.ts b/invokeai/frontend/web/src/features/modelManagerV2/models.ts index 91d72d289af..d2fee012af8 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/models.ts +++ b/invokeai/frontend/web/src/features/modelManagerV2/models.ts @@ -336,7 +336,15 @@ export const MODEL_FORMAT_TO_LONG_NAME: Record = { export const SUPPORTS_OPTIMIZED_DENOISING_BASE_MODELS: BaseModelType[] = ['flux', 'sd-3']; -export const SUPPORTS_REF_IMAGES_BASE_MODELS: BaseModelType[] = ['sd-1', 'sdxl', 'flux', 'flux2', 'qwen-image', 'wan']; +export const SUPPORTS_REF_IMAGES_BASE_MODELS: BaseModelType[] = [ + 'sd-1', + 'sdxl', + 'flux', + 'flux2', + 'qwen-image', + 'wan', + 'minimax-h3', +]; export const SUPPORTS_NEGATIVE_PROMPT_BASE_MODELS: BaseModelType[] = [ 'sd-1', diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts index dc91405d1a2..90d406f39ea 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts @@ -26,7 +26,8 @@ type AddImageToImageArg = { l2i: Invocation; i2l: Invocation; noise?: Invocation<'noise'>; - denoise: Invocation; + // MiniMax H3's denoise node has no denoising_start/end, so it cannot do img2img. + denoise: Invocation>; vaeSource: Invocation; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts index a948fd67621..de29d17333d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addTextToImage.ts @@ -33,13 +33,18 @@ export const addTextToImage = ({ | 'ernie_image_vae_decode' | 'anima_l2i' | 'wan_l2i' + | 'minimax_h3_latents_to_image' > => { - denoise.denoising_start = 0; - denoise.denoising_end = 1; + if (denoise.type !== 'minimax_h3_denoise') { + // MiniMax H3's denoise node has no denoising_start/end fields (txt2img/t2v only). + denoise.denoising_start = 0; + denoise.denoising_end = 1; + } const { originalSize, scaledSize } = getOriginalAndScaledSizesForTextToImage(state); if ( + denoise.type === 'minimax_h3_denoise' || denoise.type === 'cogview4_denoise' || denoise.type === 'qwen_image_denoise' || denoise.type === 'flux_denoise' || diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildMiniMaxH3Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildMiniMaxH3Graph.ts new file mode 100644 index 00000000000..ab8611f32f2 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildMiniMaxH3Graph.ts @@ -0,0 +1,246 @@ +import { logger } from 'app/logging/logger'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; +import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; +import { selectCanvasMetadata } from 'features/controlLayers/store/selectors'; +import { isMiniMaxH3ReferenceImageConfig } from 'features/controlLayers/store/types'; +import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/validators'; +import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; +import { zImageField } from 'features/nodes/types/common'; +import { addNSFWChecker } from 'features/nodes/util/graph/generation/addNSFWChecker'; +import { addTextToImage } from 'features/nodes/util/graph/generation/addTextToImage'; +import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermarker'; +import { Graph } from 'features/nodes/util/graph/generation/Graph'; +import { + getOriginalAndScaledSizesForTextToImage, + selectCanvasOutputFields, +} from 'features/nodes/util/graph/graphBuilderUtils'; +import type { GraphBuilderArg, GraphBuilderReturn, ImageOutputNodes } from 'features/nodes/util/graph/types'; +import { UnsupportedGenerationModeError } from 'features/nodes/util/graph/types'; +import { selectActiveTab } from 'features/ui/store/uiSelectors'; +import type { Invocation } from 'services/api/types'; +import { isNonRefinerMainModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; + +const log = logger('system'); + +/** MiniMax H3 generates at a fixed 24 fps. */ +const MINIMAX_H3_FPS = 24; +/** Legal frame counts are 17n+5. 124 frames is the 5s+ video minimum; 345 (14.375s) is the + * largest grid point within the model's 15s ceiling. The 5-frame minimum block is reserved + * for the still-image output mode. */ +const MINIMAX_H3_MIN_VIDEO_FRAMES = 124; +const MINIMAX_H3_MAX_VIDEO_FRAMES = 345; +const MINIMAX_H3_IMAGE_FRAMES = 5; + +/** + * Snap a duration in seconds to the nearest legal MiniMax H3 frame count (17n+5), clamped to + * the video range [124, 345]. + */ +const snapMiniMaxH3DurationToFrames = (durationSeconds: number): number => { + // The top slider stop (14 s) maps to the model's true ceiling (345 frames = 14.375 s): + // nearest-grid rounding alone would top out at 328 and leave the last 0.7 s unreachable. + if (durationSeconds >= 14) { + return MINIMAX_H3_MAX_VIDEO_FRAMES; + } + const targetFrames = durationSeconds * MINIMAX_H3_FPS; + const n = Math.round((targetFrames - 5) / 17); + const frames = n * 17 + 5; + return Math.min(Math.max(frames, MINIMAX_H3_MIN_VIDEO_FRAMES), MINIMAX_H3_MAX_VIDEO_FRAMES); +}; + +/** + * Build a graph for MiniMax H3 (Hailuo 3.0) generation. + * + * H3 is a joint audio-video model; the linear UI exposes two output modes: + * - 'video': text-to-video / first-frame-to-video with a muxed stereo soundtrack. Generate + * tab only. + * - 'image': a minimum-length (5-frame) clip decoded to a single gallery image. This is the + * text-to-image mode and also works as the canvas txt2img path. + * + * The checkpoint is guidance-distilled: no negative prompt, no CFG - there is exactly one + * prompt node. When a first-frame reference image is wired, the SAME image and canvas + * dimensions MUST reach both the text encoder (vision context) and the frame-conditioning + * node - the backend denoise node enforces this coupling. + */ +export const buildMiniMaxH3Graph = async (arg: GraphBuilderArg): Promise => { + const { generationMode, state, manager } = arg; + + log.debug({ generationMode, manager: manager?.id }, 'Building MiniMax H3 graph'); + + const model = selectMainModelConfig(state); + assert(model, 'No model selected'); + assert(model.base === 'minimax-h3', 'Selected model is not a MiniMax H3 model'); + const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig); + assert(modelConfig.base === 'minimax-h3'); + + const params = selectParamsSlice(state); + const { minimaxH3OutputMode } = params; + // The H3 denoise node requires steps >= 2 (N sigma grid points = N-1 model evaluations); + // the shared Steps slider allows 1, so clamp rather than 422 at enqueue. + const steps = Math.max(2, params.steps); + + if (generationMode !== 'txt2img') { + throw new UnsupportedGenerationModeError( + 'MiniMax H3 supports text-to-video, first-frame-to-video and text-to-image only. ' + + 'Canvas img2img / inpaint / outpaint are not supported.' + ); + } + + const g = new Graph(getPrefixedId('minimax_h3_graph')); + + const modelLoader = g.addNode({ + type: 'minimax_h3_model_loader', + id: getPrefixedId('minimax_h3_model_loader'), + model, + }); + + const positivePrompt = g.addNode({ + id: getPrefixedId('positive_prompt'), + type: 'string', + }); + // Guidance-distilled: no negative prompt, no CFG, one text encoder. + const posCond = g.addNode({ + type: 'minimax_h3_text_encoder', + id: getPrefixedId('pos_prompt'), + }); + + const seed = g.addNode({ + id: getPrefixedId('seed'), + type: 'integer', + }); + + const denoise = g.addNode({ + type: 'minimax_h3_denoise', + id: getPrefixedId('denoise_latents'), + steps, + }); + + g.addEdge(modelLoader, 'transformer', denoise, 'transformer'); + g.addEdge(modelLoader, 'text_encoder', posCond, 'text_encoder'); + g.addEdge(positivePrompt, 'value', posCond, 'prompt'); + g.addEdge(posCond, 'conditioning', denoise, 'positive_conditioning'); + g.addEdge(seed, 'value', denoise, 'seed'); + + g.upsertMetadata({ + model: Graph.getModelMetadataField(modelConfig), + steps, + minimax_h3_output_mode: minimaxH3OutputMode, + }); + g.addEdgeToMetadata(seed, 'value', 'seed'); + g.addEdgeToMetadata(positivePrompt, 'value', 'positive_prompt'); + + // First-frame conditioning (video mode only): the first enabled MiniMax H3 reference image + // becomes the video's first frame. The image feeds BOTH the text encoder (vision context) + // and the frame-conditioning node (VAE condition rows) - the backend requires the pair. + const refEntity = + minimaxH3OutputMode === 'video' + ? selectRefImagesSlice(state).entities.find( + (entity) => + entity.isEnabled && + isMiniMaxH3ReferenceImageConfig(entity.config) && + entity.config.image !== null && + getGlobalReferenceImageWarnings(entity, modelConfig).length === 0 + ) + : undefined; + + if (minimaxH3OutputMode === 'video') { + if (selectActiveTab(state) !== 'generate') { + throw new UnsupportedGenerationModeError('MiniMax H3 video generation runs on the Generate tab.'); + } + + const { originalSize } = getOriginalAndScaledSizesForTextToImage(state); + const num_frames = snapMiniMaxH3DurationToFrames(params.minimaxH3DurationSeconds); + + g.updateNode(denoise, { + width: originalSize.width, + height: originalSize.height, + num_frames, + }); + // The keyframe vision context is prepared on the same canvas as the condition rows. + g.updateNode(posCond, { + width: originalSize.width, + height: originalSize.height, + }); + + const l2v = g.addNode({ + type: 'minimax_h3_latents_to_video', + id: getPrefixedId('l2v'), + }); + g.addEdge(modelLoader, 'vae', l2v, 'vae'); + g.addEdge(modelLoader, 'audio_vae', l2v, 'audio_vae'); + g.addEdge(denoise, 'video_latents', l2v, 'video_latents'); + g.addEdge(denoise, 'audio_latents', l2v, 'audio_latents'); + + if (refEntity) { + assert(isMiniMaxH3ReferenceImageConfig(refEntity.config) && refEntity.config.image); + const refImageField = zImageField.parse( + refEntity.config.image.crop?.image ?? refEntity.config.image.original.image + ); + const frameCond = g.addNode({ + type: 'minimax_h3_frame_conditioning', + id: getPrefixedId('minimax_h3_frame_conditioning'), + first_image: refImageField, + width: originalSize.width, + height: originalSize.height, + }); + g.addEdge(modelLoader, 'vae', frameCond, 'vae'); + g.addEdge(frameCond, 'frame_conditioning', denoise, 'frame_conditioning'); + g.updateNode(posCond, { first_image: refImageField }); + g.upsertMetadata({ generation_mode: 'minimax_h3_i2v' }); + } else { + g.upsertMetadata({ generation_mode: 'minimax_h3_t2v' }); + } + + g.upsertMetadata({ + width: originalSize.width, + height: originalSize.height, + minimax_h3_duration_seconds: params.minimaxH3DurationSeconds, + }); + + g.updateNode(l2v, selectCanvasOutputFields(state)); + g.setMetadataReceivingNode(l2v); + + return { + g, + seed, + positivePrompt, + }; + } + + // Image output mode: a 5-frame (minimum block) clip decoded to a single image. Works on + // both the Generate tab and canvas txt2img. + g.updateNode(denoise, { num_frames: MINIMAX_H3_IMAGE_FRAMES }); + + const l2i = g.addNode({ + type: 'minimax_h3_latents_to_image', + id: getPrefixedId('l2i'), + frame_index: 0, + }); + g.addEdge(modelLoader, 'vae', l2i, 'vae'); + g.addEdge(denoise, 'video_latents', l2i, 'video_latents'); + + let canvasOutput: Invocation = addTextToImage({ g, state, denoise, l2i }); + g.upsertMetadata({ generation_mode: 'minimax_h3_txt2img' }); + + if (state.system.shouldUseNSFWChecker) { + canvasOutput = addNSFWChecker(g, canvasOutput); + } + if (state.system.shouldUseWatermarker) { + canvasOutput = addWatermarker(g, canvasOutput); + } + + g.updateNode(canvasOutput, selectCanvasOutputFields(state)); + + if (selectActiveTab(state) === 'canvas') { + g.upsertMetadata(selectCanvasMetadata(state)); + } + + g.setMetadataReceivingNode(canvasOutput); + + return { + g, + seed, + positivePrompt, + }; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/types.ts b/invokeai/frontend/web/src/features/nodes/util/graph/types.ts index 783d58d2e09..cb5e9fdb17e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/types.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/types.ts @@ -26,7 +26,8 @@ export type ImageOutputNodes = | 'ernie_image_vae_decode' | 'ideogram4_l2i' | 'anima_l2i' - | 'wan_l2i'; + | 'wan_l2i' + | 'minimax_h3_latents_to_image'; export type LatentToImageNodes = | 'l2i' @@ -38,7 +39,8 @@ export type LatentToImageNodes = | 'z_image_l2i' | 'ernie_image_vae_decode' | 'anima_l2i' - | 'wan_l2i'; + | 'wan_l2i' + | 'minimax_h3_latents_to_image'; export type ImageToLatentsNodes = | 'i2l' @@ -62,13 +64,15 @@ export type DenoiseLatentsNodes = | 'ernie_image_denoise' | 'krea2_denoise' | 'anima_denoise' - | 'wan_denoise'; + | 'wan_denoise' + | 'minimax_h3_denoise'; /** * Denoise nodes that support masked denoising (inpaint/outpaint). ERNIE-Image's denoise node - * has no `denoise_mask` input (it is text-to-image only), so it is excluded here. + * has no `denoise_mask` input (it is text-to-image only), and MiniMax H3's denoise node has + * neither `denoise_mask` nor `denoising_start/end` (txt2img/t2v only), so they are excluded. */ -export type MaskableDenoiseNodes = Exclude; +export type MaskableDenoiseNodes = Exclude; export type MainModelLoaderNodes = | 'main_model_loader' @@ -82,7 +86,8 @@ export type MainModelLoaderNodes = | 'ernie_image_model_loader' | 'krea2_model_loader' | 'anima_model_loader' - | 'wan_model_loader'; + | 'wan_model_loader' + | 'minimax_h3_model_loader'; export type VaeSourceNodes = 'seamless' | 'vae_loader'; diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamMiniMaxH3Duration.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamMiniMaxH3Duration.tsx new file mode 100644 index 00000000000..b379375a45d --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamMiniMaxH3Duration.tsx @@ -0,0 +1,66 @@ +import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { + minimaxH3DurationSecondsChanged, + selectMiniMaxH3DurationSeconds, + selectMiniMaxH3OutputMode, +} from 'features/controlLayers/store/paramsSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +// MiniMax H3 runs at a fixed 24 fps and frame counts snap to the 17n+5 grid. The slider works +// in whole seconds; the graph builder snaps to the nearest legal frame count, and the 14 s +// stop maps to the model's true ceiling (345 frames = 14.375 s). +const CONSTRAINTS = { + initial: 5, + sliderMin: 5, + sliderMax: 14, + fineStep: 1, + coarseStep: 1, +}; + +const MARKS = [5, 10, 14]; + +/** + * MiniMax H3 video duration in seconds. Only shown in the 'video' output mode - the 'image' + * mode always generates the 5-frame minimum block. + */ +const ParamMiniMaxH3Duration = () => { + const { t } = useTranslation(); + const duration = useAppSelector(selectMiniMaxH3DurationSeconds); + const outputMode = useAppSelector(selectMiniMaxH3OutputMode); + const dispatch = useAppDispatch(); + + const onChange = useCallback((v: number) => dispatch(minimaxH3DurationSecondsChanged(v)), [dispatch]); + + if (outputMode !== 'video') { + return null; + } + + return ( + + {t('parameters.minimaxH3DurationSeconds')} + + + + ); +}; + +export default memo(ParamMiniMaxH3Duration); diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamMiniMaxH3OutputMode.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamMiniMaxH3OutputMode.tsx new file mode 100644 index 00000000000..df4b82c9629 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamMiniMaxH3OutputMode.tsx @@ -0,0 +1,34 @@ +import { Button, ButtonGroup, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { minimaxH3OutputModeChanged, selectMiniMaxH3OutputMode } from 'features/controlLayers/store/paramsSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +/** + * MiniMax H3 output mode toggle: 'video' (joint audio-video, Generate tab only) or 'image' + * (a 5-frame minimum clip decoded to one gallery image - the txt2img mode, canvas-capable). + */ +const ParamMiniMaxH3OutputMode = () => { + const { t } = useTranslation(); + const outputMode = useAppSelector(selectMiniMaxH3OutputMode); + const dispatch = useAppDispatch(); + + const onClickVideo = useCallback(() => dispatch(minimaxH3OutputModeChanged('video')), [dispatch]); + const onClickImage = useCallback(() => dispatch(minimaxH3OutputModeChanged('image')), [dispatch]); + + return ( + + {t('parameters.minimaxH3OutputMode')} + + + + + + ); +}; + +export default memo(ParamMiniMaxH3OutputMode); diff --git a/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts b/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts index 1777033e6b2..b503510ca9b 100644 --- a/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts +++ b/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts @@ -33,6 +33,9 @@ export const getOptimalDimension = (base?: BaseModelType | null, pidScale = 1): case 'sd-1': case 'sd-2': return 512; + case 'minimax-h3': + // Native canvas has a 768px short edge (soft cap 768x1344). + return 768; case 'sdxl': case 'flux': case 'flux2': @@ -107,6 +110,11 @@ export const getGridSize = (base?: BaseModelType | null, pidScale = 1): number = case 'cogview4': gridSize = 32; break; + case 'minimax-h3': + // The H3 denoise node hard-validates width/height as multiples of 32 + // (16x VAE spatial compression x patch size 2). + gridSize = 32; + break; case 'flux': case 'flux2': case 'sd-3': diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts index e8c823d1c5f..cd24e57ec28 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts @@ -20,6 +20,7 @@ import { buildExternalGraph } from 'features/nodes/util/graph/generation/buildEx import { buildFLUXGraph } from 'features/nodes/util/graph/generation/buildFLUXGraph'; import { buildIdeogram4Graph } from 'features/nodes/util/graph/generation/buildIdeogram4Graph'; import { buildKrea2Graph } from 'features/nodes/util/graph/generation/buildKrea2Graph'; +import { buildMiniMaxH3Graph } from 'features/nodes/util/graph/generation/buildMiniMaxH3Graph'; import { buildQwenImageGraph } from 'features/nodes/util/graph/generation/buildQwenImageGraph'; import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph'; import { buildSD3Graph } from 'features/nodes/util/graph/generation/buildSD3Graph'; @@ -85,6 +86,8 @@ const enqueueCanvas = async (store: AppStore, canvasManager: CanvasManager, prep return await buildAnimaGraph(graphBuilderArg); case 'wan': return await buildWanGraph(graphBuilderArg); + case 'minimax-h3': + return await buildMiniMaxH3Graph(graphBuilderArg); default: assert(false, `No graph builders for base ${base}`); } diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts index 65309420664..6f2c770dc63 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts @@ -18,6 +18,7 @@ import { buildExternalGraph } from 'features/nodes/util/graph/generation/buildEx import { buildFLUXGraph } from 'features/nodes/util/graph/generation/buildFLUXGraph'; import { buildIdeogram4Graph } from 'features/nodes/util/graph/generation/buildIdeogram4Graph'; import { buildKrea2Graph } from 'features/nodes/util/graph/generation/buildKrea2Graph'; +import { buildMiniMaxH3Graph } from 'features/nodes/util/graph/generation/buildMiniMaxH3Graph'; import { buildQwenImageGraph } from 'features/nodes/util/graph/generation/buildQwenImageGraph'; import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph'; import { buildSD3Graph } from 'features/nodes/util/graph/generation/buildSD3Graph'; @@ -78,6 +79,8 @@ const enqueueGenerate = async (store: AppStore, prepend: boolean) => { return await buildAnimaGraph(graphBuilderArg); case 'wan': return await buildWanGraph(graphBuilderArg); + case 'minimax-h3': + return await buildMiniMaxH3Graph(graphBuilderArg); default: assert(false, `No graph builders for base ${base}`); } diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts index d479e2d506f..fd8a74e2c79 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts @@ -1169,6 +1169,12 @@ export const getReasonsWhyCannotEnqueueCanvasTab = (arg: { } } + if (model?.base === 'minimax-h3' && params.minimaxH3OutputMode === 'video') { + // Video output only exists on the Generate tab - the canvas compositing pipeline is + // image-based. The image output mode works on canvas like any other txt2img model. + reasons.push({ content: i18n.t('parameters.invoke.minimaxH3VideoOnGenerateTab') }); + } + if (model) { for (const lora of loras.filter(({ isEnabled }) => isEnabled === true)) { if (model.base !== lora.model.base) { diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx index 188dc9e47c0..080418a97f1 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx @@ -14,6 +14,7 @@ import { selectIsFlux2, selectIsIdeogram4, selectIsKrea2, + selectIsMiniMaxH3, selectIsQwenImage, selectIsWan, selectIsZImage, @@ -32,6 +33,8 @@ import ParamFluxDypeScale from 'features/parameters/components/Core/ParamFluxDyp import ParamFluxScheduler from 'features/parameters/components/Core/ParamFluxScheduler'; import ParamGuidance from 'features/parameters/components/Core/ParamGuidance'; import ParamIdeogram4SamplerPreset from 'features/parameters/components/Core/ParamIdeogram4SamplerPreset'; +import ParamMiniMaxH3Duration from 'features/parameters/components/Core/ParamMiniMaxH3Duration'; +import ParamMiniMaxH3OutputMode from 'features/parameters/components/Core/ParamMiniMaxH3OutputMode'; import ParamQwenImageShift from 'features/parameters/components/Core/ParamQwenImageShift'; import ParamScheduler from 'features/parameters/components/Core/ParamScheduler'; import ParamSteps from 'features/parameters/components/Core/ParamSteps'; @@ -69,6 +72,7 @@ export const GenerationSettingsAccordion = memo(() => { const isKrea2 = useAppSelector(selectIsKrea2); const isAnima = useAppSelector(selectIsAnima); const isWan = useAppSelector(selectIsWan); + const isMiniMaxH3 = useAppSelector(selectIsMiniMaxH3); const fluxDypePreset = useAppSelector(selectFluxDypePreset); const modelSupportsGuidance = useAppSelector(selectModelSupportsGuidance); const modelSupportsSteps = useAppSelector(selectModelSupportsSteps); @@ -124,8 +128,10 @@ export const GenerationSettingsAccordion = memo(() => { {!isExternal && isFLUX && modelConfig && !isFluxFillMainModelModelConfig(modelConfig) && ( )} - {!isExternal && !isFLUX && !isFlux2 && !isIdeogram4 && } + {!isExternal && !isFLUX && !isFlux2 && !isIdeogram4 && !isMiniMaxH3 && } {!isExternal && isWan && } + {!isExternal && isMiniMaxH3 && } + {!isExternal && isMiniMaxH3 && } {!isExternal && isZImage && } {!isExternal && isQwenImage && } {!isExternal && isFLUX && } diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/generationSettingsVisibility.ts b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/generationSettingsVisibility.ts index 304bf8f79ab..9ccbe304493 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/generationSettingsVisibility.ts +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/generationSettingsVisibility.ts @@ -13,6 +13,7 @@ const BASES_WITHOUT_STANDARD_SCHEDULER = new Set([ 'krea-2', 'wan', 'ideogram-4', + 'minimax-h3', ]); export const shouldShowStandardScheduler = (base: BaseModelType | null | undefined): boolean =>