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
14 changes: 12 additions & 2 deletions invokeai/app/services/model_install/model_install_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -1377,9 +1377,13 @@ def _multifile_download(
# subdirectory within the model folder.

if subfolders and len(subfolders) > 1:
# Multiple subfolders: create combined name and keep subfolder structure
# Multiple subfolders: create combined name and keep subfolder structure. Entries may
# also be explicit files (e.g. "modular_model_index.json" or "transformer/config.json");
# use their stems in the combined name so it stays a sane directory name.
top = Path(remote_files[0].path.parts[0]) # e.g. "Z-Image-Turbo/"
subfolder_names = [sf.name.replace("/", "_").replace("\\", "_") for sf in subfolders]
subfolder_names = [
(sf.stem if sf.suffix else sf.name).replace("/", "_").replace("\\", "_") for sf in subfolders
]
combined_name = "_".join(subfolder_names)
path_to_add = Path(f"{top}_{combined_name}")

Expand All @@ -1390,6 +1394,12 @@ def _multifile_download(
file_path = model_file.path
new_path: Optional[Path] = None
for sf in subfolders:
if file_path == top / sf:
# An explicit file entry: keep its repo-relative path so e.g.
# transformer/config.json stays inside transformer/. (relative_to() below
# would return "." here and flatten the file to the model root.)
new_path = path_to_add / sf
break
try:
# Try to get relative path from this subfolder
relative = file_path.relative_to(top / sf)
Expand Down
18 changes: 18 additions & 0 deletions invokeai/backend/model_manager/configs/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1487,6 +1487,12 @@ class Main_Diffusers_MiniMaxH3_Config(Diffusers_Config_Base, Main_Config_Base, C

base: Literal[BaseModelType.MiniMaxH3] = Field(BaseModelType.MiniMaxH3)
variant: MiniMaxH3VariantType = Field()
components_only: bool = Field(
default=False,
description="Whether the folder holds only the shared components (tokenizer, processor, VAEs) "
"without transformer weights - a slim install whose transformer and text encoder must be "
"supplied as single-file overrides at generation time.",
)

@classmethod
def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self:
Expand Down Expand Up @@ -1515,10 +1521,22 @@ def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -

repo_variant = override_fields.pop("repo_variant", None) or cls._get_repo_variant_or_raise(mod)

# A slim ("components-only") install carries the transformer's config.json for variant
# identification but no weight shards - the transformer and text encoder come from
# single-file installs selected in the model loader instead. Record that here so the UI
# can require those selections up front rather than failing mid-generation.
components_only = override_fields.pop("components_only", None)
if components_only is None:
transformer_dir = mod.path / "transformer"
components_only = not any(
any(transformer_dir.glob(pattern)) for pattern in ("*.safetensors", "*.bin", "*.pth", "*.pt", "*.ckpt")
)

return cls(
**override_fields,
variant=variant,
repo_variant=repo_variant,
components_only=components_only,
)

@classmethod
Expand Down
20 changes: 20 additions & 0 deletions invokeai/backend/model_manager/load/model_loaders/minimax_h3.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,22 @@
from invokeai.backend.util.devices import TorchDevice


def _raise_if_no_weight_shards(submodel_path: Path, submodel_label: str) -> None:
"""Fail with an actionable message when a components-only (slim) install is asked for a
submodel whose weights it does not carry.

A slim MiniMax H3 install ships the shared components (tokenizer, processor, VAEs) plus bare
config JSONs, so a generic from_pretrained() here would otherwise die on a cryptic
missing-shard error deep inside diffusers/transformers.
"""
if not any(any(submodel_path.glob(pattern)) for pattern in ("*.safetensors", "*.bin", "*.pth", "*.pt", "*.ckpt")):
raise ValueError(
f"This MiniMax H3 model folder has no {submodel_label} weights - it is a components-only "
f"(slim) install. In the MiniMax H3 Model Loader, select a single-file {submodel_label} "
f"(for example the Comfy-Org int8 release) to use with it."
)


@ModelLoaderRegistry.register(base=BaseModelType.MiniMaxH3, type=ModelType.Main, format=ModelFormat.Diffusers)
class MiniMaxH3DiffusersModel(ModelLoader):
"""Loader for MiniMax H3 diffusers-format models (FL2VA)."""
Expand All @@ -67,12 +83,16 @@ def _load_model(

match submodel_type:
case SubModelType.Transformer:
_raise_if_no_weight_shards(submodel_path, "transformer")

from invokeai.backend.minimax_h3 import MiniMaxH3Transformer3DModel

return MiniMaxH3Transformer3DModel.from_pretrained(
submodel_path, torch_dtype=dtype, local_files_only=True
)
case SubModelType.TextEncoder:
_raise_if_no_weight_shards(submodel_path, "text encoder")

from transformers import AutoConfig, Qwen3VLForConditionalGeneration

te_config = normalize_qwen3vl_rope_config(
Expand Down
79 changes: 60 additions & 19 deletions invokeai/backend/model_manager/starter_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1740,25 +1740,53 @@ def _gemini_3_resolution_presets(
# endregion

# region MiniMax H3 (local)
# TODO(minimax-h3): no starter entry yet — the installer cannot express a viable download.
# MiniMax H3 FL2VA lives in huggingface.co/MiniMaxAI/MiniMax-H3 as a root-level Modular
# Diffusers layout whose probe requires the root `modular_model_index.json`, but the repo also
# carries the Ref2VA transformer (`transformer_ref/`) and the original remote-code checkpoints
# (`FL2VA/`, `Ref2VA/`) as siblings:
# - a bare `MiniMaxAI/MiniMax-H3` source downloads ~498 GB (every subtree matches
# `filter_files`' weight patterns) for a ~42.5 GB working set;
# - a `::transformer+text_encoder+tokenizer+processor+vae+audio_vae` subfolder source skips
# the root `modular_model_index.json` (`filter_files` keeps only files INSIDE the listed
# subfolders), so identification fails and the install lands as an unknown model.
# Unblocking this needs either (a) `filter_files` learning to include root config JSONs
# alongside a subfolder set — an installer-wide behavior change that would also alter what
# existing `::subfolder` sources (e.g. the Wan T5 encoder) download — or (b) an upstream
# FL2VA-only diffusers repo. Until then, users install by pointing the Model Manager at a
# locally assembled root-layout folder (see the "MiniMax H3" default workflows' notes).
# Any future entry must keep "MiniMax H3" verbatim in its name/description (the MiniMax H3
# Community License requires prominent attribution) and should note the license's territory
# restrictions (excludes the US, EU, UK and South Korea, extending to outputs), which is why
# this region is isolated in its own droppable commit.
# License note: the MiniMax H3 Community License requires prominent "MiniMax H3" attribution
# (keep it verbatim in every name/description below) and restricts use by territory (excludes
# the US, EU, UK and South Korea, extending to outputs). These entries live in their own
# droppable commit so a release can exclude them without touching anything else.
#
# The full huggingface.co/MiniMaxAI/MiniMax-H3 repo is ~498 GB (it also carries the Ref2VA
# transformer and the original remote-code checkpoints). The slim main below downloads only the
# shared components (tokenizer, processor, video/audio VAEs) plus the two config JSONs that
# identification needs (~11 GB); the transformer and text encoder come from Comfy-Org's int8
# single-file repacks, selected in the MiniMax H3 Model Loader. Total ~59 GB.

minimax_h3_components = StarterModel(
name="MiniMax H3 Components",
base=BaseModelType.MiniMaxH3,
source="MiniMaxAI/MiniMax-H3::modular_model_index.json+transformer/config.json+tokenizer+processor+vae+audio_vae",
description="MiniMax H3 shared components: tokenizer, processor and video/audio VAEs, without "
"transformer or text-encoder weights (~11 GB). Pair with the MiniMax H3 single-file transformer "
"and text encoder. NOTE: This model is distributed under a restrictive license that forbids its "
"use in certain territories. Please see https://huggingface.co/MiniMaxAI/MiniMax-H3 for details.",
type=ModelType.Main,
format=ModelFormat.Diffusers,
)

minimax_h3_int8_text_encoder = StarterModel(
name="MiniMax H3 Text Encoder (int8)",
base=BaseModelType.MiniMaxH3,
source="Comfy-Org/MiniMax-H3::text_encoders/qwen3vl_32b_minimax_h3_int8_convrot.safetensors",
description="Truncated Qwen3-VL-32B conditioning encoder for MiniMax H3, int8 quantized (~27 GB). "
"Select it in the MiniMax H3 Model Loader's text encoder field. NOTE: This model is distributed "
"under a restrictive license that forbids its use in certain territories. Please see "
"https://huggingface.co/MiniMaxAI/MiniMax-H3 for details.",
type=ModelType.Qwen3VLEncoder,
format=ModelFormat.Checkpoint,
)

minimax_h3_int8_transformer = StarterModel(
name="MiniMax H3 FL2VA Transformer (int8, pruned)",
base=BaseModelType.MiniMaxH3,
source="Comfy-Org/MiniMax-H3::diffusion_models/minimax_h3_fl2va_pruned_int8_convrot.safetensors",
description="MiniMax H3 video+audio generation. AdaLN-pruned int8 single-file transformer (~21 GB); "
"select it in the MiniMax H3 Model Loader's transformer field. Total size with dependencies: ~59 GB. "
"NOTE: This model is distributed under a restrictive license that forbids its use in certain "
"territories. Please see https://huggingface.co/MiniMaxAI/MiniMax-H3 for details.",
type=ModelType.Main,
format=ModelFormat.Checkpoint,
dependencies=[minimax_h3_components, minimax_h3_int8_text_encoder],
)
# endregion

alibabacloud_wan26_t2i = StarterModel(
Expand Down Expand Up @@ -2261,6 +2289,9 @@ def _gemini_3_resolution_presets(
wan_22_ti2v_5b_diffusers,
wan_22_ti2v_5b_gguf_q4_k_m,
wan_22_ti2v_5b_gguf_q8_0,
minimax_h3_int8_transformer,
minimax_h3_int8_text_encoder,
minimax_h3_components,
gemini_flash_image,
gemini_pro_image_preview,
gemini_3_1_flash_image_preview,
Expand Down Expand Up @@ -2432,6 +2463,15 @@ def _gemini_3_resolution_presets(
ideogram_4_nf4,
]

# The minimal working set for MiniMax H3 video+audio generation (~59 GB): shared components from
# the official repo plus Comfy-Org's int8 single-file transformer and text encoder. See the
# license note in the MiniMax H3 region above.
minimax_h3_bundle: list[StarterModel] = [
minimax_h3_components,
minimax_h3_int8_text_encoder,
minimax_h3_int8_transformer,
]

STARTER_BUNDLES: dict[str, StarterModelBundle] = {
BaseModelType.StableDiffusion1: StarterModelBundle(name="Stable Diffusion 1.5", models=sd1_bundle),
BaseModelType.StableDiffusionXL: StarterModelBundle(name="SDXL", models=sdxl_bundle),
Expand All @@ -2444,6 +2484,7 @@ def _gemini_3_resolution_presets(
BaseModelType.Krea2: StarterModelBundle(name="Krea-2", models=krea2_bundle),
"wan_t2v": StarterModelBundle(name="Wan 2.2 Text-to-Video", models=wan_t2v_bundle),
"wan_i2v": StarterModelBundle(name="Wan 2.2 Image-to-Video", models=wan_i2v_bundle),
BaseModelType.MiniMaxH3: StarterModelBundle(name="MiniMax H3", models=minimax_h3_bundle),
BaseModelType.Ideogram4: StarterModelBundle(name="Ideogram 4", models=ideogram_bundle),
}

Expand Down
22 changes: 19 additions & 3 deletions invokeai/backend/model_manager/util/select_hf_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ def filter_files(
:param files: List of files relative to the repo root.
:param subfolder: Filter by the indicated subfolder (deprecated, use subfolders instead).
:param subfolders: Filter by multiple subfolders. Files from any of these subfolders will be included.
An entry that names a repo file exactly (e.g. "modular_model_index.json" or
"transformer/config.json") is an explicit file request: it is included verbatim, bypassing
the extension prefilter, variant scoring and config-only-folder pruning that apply to
subfolder contents.
:param variant: Filter by files belonging to a particular variant, such as fp16.

The file list can be obtained from the `files` field of HuggingFaceMetadata,
Expand All @@ -58,6 +62,17 @@ def filter_files(
if sf.suffix in [".safetensors", ".bin", ".onnx", ".xml", ".pth", ".pt", ".ckpt", ".msgpack"]:
return [root / sf]

# In a multi-entry list, entries that name a repo file exactly are explicit requests for that
# file. Split them out so they skip every filter below - a slim install can then combine
# component subfolders with the root pipeline index and a bare config.json (whose folder the
# config-only pruning at the end would otherwise drop).
subfolder_filter_requested = bool(filter_subfolders)
explicit_files: List[Path] = []
if len(filter_subfolders) > 1:
file_set = set(files)
explicit_files = [sf for sf in filter_subfolders if (root / sf) in file_set]
filter_subfolders = [sf for sf in filter_subfolders if sf not in explicit_files]

# Start by filtering on model file extensions, discarding images, docs, etc
for file in files:
if file.name.endswith((".json", ".txt", ".jinja")): # .jinja for chat templates
Expand All @@ -81,13 +96,14 @@ def filter_files(
elif re.search(r"model.*\.(safetensors|bin|onnx|xml|pth|pt|ckpt|msgpack)$", file.name):
paths.append(file)

# limit search to subfolder(s) if requested
if filter_subfolders:
# limit search to subfolder(s) if requested. When every entry was an explicit file, this
# correctly reduces the subfolder-derived selection to nothing rather than the whole repo.
if subfolder_filter_requested:
absolute_subfolders = [root / sf for sf in filter_subfolders]
paths = [x for x in paths if any(Path(sf) in x.parents for sf in absolute_subfolders)]

# _filter_by_variant uniquifies the paths and returns a set
return sorted(_filter_by_variant(paths, variant))
return sorted(_filter_by_variant(paths, variant) | {root / sf for sf in explicit_files})


@dataclass
Expand Down
9 changes: 8 additions & 1 deletion invokeai/frontend/web/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -59080,6 +59080,12 @@
},
"variant": {
"$ref": "#/components/schemas/MiniMaxH3VariantType"
},
"components_only": {
"type": "boolean",
"title": "Components Only",
"description": "Whether the folder holds only the shared components (tokenizer, processor, VAEs) without transformer weights - a slim install whose transformer and text encoder must be supplied as single-file overrides at generation time.",
"default": false
}
},
"type": "object",
Expand All @@ -59101,7 +59107,8 @@
"format",
"repo_variant",
"base",
"variant"
"variant",
"components_only"
],
"title": "Main_Diffusers_MiniMaxH3_Config",
"description": "Model config for MiniMax H3 (Hailuo 3.0) diffusers-format models."
Expand Down
2 changes: 2 additions & 0 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1788,6 +1788,8 @@
"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)",
"noMiniMaxH3TransformerModelSelected": "Components-only MiniMax H3 install: select a single-file Transformer in Advanced settings",
"noMiniMaxH3TextEncoderModelSelected": "Components-only MiniMax H3 install: select a single-file Text Encoder in Advanced settings",
"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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
fluxVAESelected,
krea2Qwen3VlEncoderModelSelected,
krea2VaeModelSelected,
minimaxH3TextEncoderModelSelected,
minimaxH3TransformerModelSelected,
modelChanged,
refinerModelChanged,
t5EncoderModelSelected,
Expand Down Expand Up @@ -84,6 +86,7 @@ export const addModelsLoadedListener = (startAppListening: AppStartListening) =>

handleMainModels(models, state, dispatch, log);
handleKrea2Components(models, state, dispatch, log);
handleMiniMaxH3Overrides(models, state, dispatch, log);
handleRefinerModels(models, state, dispatch, log);
handleVAEModels(models, state, dispatch, log);
handleLoRAModels(models, state, dispatch, log);
Expand Down Expand Up @@ -125,6 +128,19 @@ export const handleKrea2Components: ModelHandler = (models, state, dispatch) =>
}
};

export const handleMiniMaxH3Overrides: ModelHandler = (models, state, dispatch) => {
// The MiniMax H3 single-file transformer / text-encoder overrides are optional (null = use the
// main folder's submodels), so never auto-select - but a selection whose model was uninstalled
// must be cleared, or it passes the components-only readiness gate and fails at invoke time.
const { minimaxH3TransformerModel, minimaxH3TextEncoderModel } = state.params;
if (minimaxH3TransformerModel && !models.some((m) => m.key === minimaxH3TransformerModel.key)) {
dispatch(minimaxH3TransformerModelSelected(null));
}
if (minimaxH3TextEncoderModel && !models.some((m) => m.key === minimaxH3TextEncoderModel.key)) {
dispatch(minimaxH3TextEncoderModelSelected(null));
}
};

type ModelHandler = (
models: AnyModelConfig[],
state: RootState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,22 @@ export const isExternalModelUnsupportedForTab = (model: AnyModelConfigWithExtern

return false;
};

/**
* Some type=main configs are not selectable primary models: they fill a secondary slot in a
* family's advanced section, and selecting them as the main model always fails at load time.
* Every picker that offers main models must filter with this predicate.
*/
export const isSecondarySlotMainModelConfig = (c: AnyModelConfigWithExternal): boolean => {
// Low-noise Wan GGUFs belong in the Transformer (Low Noise) slot of the Wan advanced section,
// not as a primary main - filter them out so users can't accidentally wire them backwards.
if (c.type === 'main' && c.base === 'wan' && c.format === 'gguf_quantized' && 'expert' in c && c.expert === 'low') {
return true;
}
// MiniMax H3 single-file transformers belong in the Transformer (single file) slot of the
// MiniMax H3 advanced section - they carry no text encoder or VAEs.
if (c.type === 'main' && c.base === 'minimax-h3' && c.format === 'checkpoint') {
return true;
}
return false;
};
Loading
Loading