Files
wehub-resource-sync cddb07a176
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:06 +08:00

343 lines
14 KiB
Python

from typing import (
Literal,
Self,
)
from pydantic import BaseModel, ConfigDict, Field
from typing_extensions import Any
from invokeai.backend.flux.controlnet.state_dict_utils import (
is_state_dict_instantx_controlnet,
is_state_dict_xlabs_controlnet,
)
from invokeai.backend.model_manager.configs.base import Checkpoint_Config_Base, Config_Base, Diffusers_Config_Base
from invokeai.backend.model_manager.configs.identification_utils import (
NotAMatchError,
common_config_paths,
get_config_dict_or_raise,
raise_for_class_name,
raise_for_override_fields,
raise_if_not_dir,
raise_if_not_file,
state_dict_has_any_keys_starting_with,
)
from invokeai.backend.model_manager.model_on_disk import ModelOnDisk
from invokeai.backend.model_manager.taxonomy import (
BaseModelType,
ModelFormat,
ModelType,
)
MODEL_NAME_TO_PREPROCESSOR = {
"canny": "canny_image_processor",
"mlsd": "mlsd_image_processor",
"depth": "depth_anything_image_processor",
"bae": "normalbae_image_processor",
"normal": "normalbae_image_processor",
"sketch": "pidi_image_processor",
"scribble": "lineart_image_processor",
"lineart anime": "lineart_anime_image_processor",
"lineart_anime": "lineart_anime_image_processor",
"lineart": "lineart_image_processor",
"soft": "hed_image_processor",
"softedge": "hed_image_processor",
"hed": "hed_image_processor",
"shuffle": "content_shuffle_image_processor",
"pose": "dw_openpose_image_processor",
"mediapipe": "mediapipe_face_processor",
"pidi": "pidi_image_processor",
"zoe": "zoe_depth_image_processor",
"color": "color_map_image_processor",
}
class ControlAdapterDefaultSettings(BaseModel):
# This could be narrowed to controlnet processor nodes, but they change. Leaving this a string is safer.
preprocessor: str | None
fp8_storage: bool | None = Field(
default=None,
description="Store weights in FP8 to reduce VRAM usage (~50% savings). Weights are cast to compute dtype during inference.",
)
model_config = ConfigDict(extra="forbid")
@classmethod
def from_model_name(cls, model_name: str) -> Self:
for k, v in MODEL_NAME_TO_PREPROCESSOR.items():
model_name_lower = model_name.lower()
if k in model_name_lower:
return cls(preprocessor=v)
return cls(preprocessor=None)
class ControlNet_Diffusers_Config_Base(Diffusers_Config_Base):
"""Model config for ControlNet models (diffusers version)."""
type: Literal[ModelType.ControlNet] = Field(default=ModelType.ControlNet)
format: Literal[ModelFormat.Diffusers] = Field(default=ModelFormat.Diffusers)
default_settings: ControlAdapterDefaultSettings | None = Field(None)
@classmethod
def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self:
raise_if_not_dir(mod)
raise_for_override_fields(cls, override_fields)
raise_for_class_name(
common_config_paths(mod.path),
{
"ControlNetModel",
"FluxControlNetModel",
},
)
cls._validate_base(mod)
repo_variant = {"repo_variant": override_fields.get("repo_variant", cls._get_repo_variant_or_raise(mod))}
args = override_fields | repo_variant
return cls(**args)
@classmethod
def _validate_base(cls, mod: ModelOnDisk) -> None:
"""Raise `NotAMatch` if the model base does not match this config class."""
expected_base = cls.model_fields["base"].default
recognized_base = cls._get_base_or_raise(mod)
if expected_base is not recognized_base:
raise NotAMatchError(f"base is {recognized_base}, not {expected_base}")
@classmethod
def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType:
config_dict = get_config_dict_or_raise(common_config_paths(mod.path))
if config_dict.get("_class_name") == "FluxControlNetModel":
return BaseModelType.Flux
dimension = config_dict.get("cross_attention_dim")
match dimension:
case 768:
return BaseModelType.StableDiffusion1
case 1024:
# No obvious way to distinguish between sd2-base and sd2-768, but we don't really differentiate them
# anyway.
return BaseModelType.StableDiffusion2
case 2048:
return BaseModelType.StableDiffusionXL
case _:
raise NotAMatchError(f"unrecognized cross_attention_dim {dimension}")
class ControlNet_Diffusers_SD1_Config(ControlNet_Diffusers_Config_Base, Config_Base):
base: Literal[BaseModelType.StableDiffusion1] = Field(default=BaseModelType.StableDiffusion1)
class ControlNet_Diffusers_SD2_Config(ControlNet_Diffusers_Config_Base, Config_Base):
base: Literal[BaseModelType.StableDiffusion2] = Field(default=BaseModelType.StableDiffusion2)
class ControlNet_Diffusers_SDXL_Config(ControlNet_Diffusers_Config_Base, Config_Base):
base: Literal[BaseModelType.StableDiffusionXL] = Field(default=BaseModelType.StableDiffusionXL)
class ControlNet_Diffusers_FLUX_Config(ControlNet_Diffusers_Config_Base, Config_Base):
base: Literal[BaseModelType.Flux] = Field(default=BaseModelType.Flux)
class ControlNet_Checkpoint_Config_Base(Checkpoint_Config_Base):
"""Model config for ControlNet models (diffusers version)."""
type: Literal[ModelType.ControlNet] = Field(default=ModelType.ControlNet)
format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint)
default_settings: ControlAdapterDefaultSettings | None = Field(None)
@classmethod
def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self:
raise_if_not_file(mod)
raise_for_override_fields(cls, override_fields)
cls._validate_looks_like_controlnet(mod)
cls._validate_base(mod)
return cls(**override_fields)
@classmethod
def _validate_base(cls, mod: ModelOnDisk) -> None:
"""Raise `NotAMatch` if the model base does not match this config class."""
expected_base = cls.model_fields["base"].default
recognized_base = cls._get_base_or_raise(mod)
if expected_base is not recognized_base:
raise NotAMatchError(f"base is {recognized_base}, not {expected_base}")
@classmethod
def _validate_looks_like_controlnet(cls, mod: ModelOnDisk) -> None:
if not state_dict_has_any_keys_starting_with(
mod.load_state_dict(),
{
"controlnet",
"control_model",
"input_blocks",
# XLabs FLUX ControlNet models have keys starting with "controlnet_blocks."
# For example: https://huggingface.co/XLabs-AI/flux-controlnet-collections/blob/86ab1e915a389d5857135c00e0d350e9e38a9048/flux-canny-controlnet_v2.safetensors
# TODO(ryand): This is very fragile. XLabs FLUX ControlNet models also contain keys starting with
# "double_blocks.", which we check for above. But, I'm afraid to modify this logic because it is so
# delicate.
"controlnet_blocks",
},
):
raise NotAMatchError("state dict does not look like a ControlNet checkpoint")
@classmethod
def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType:
state_dict = mod.load_state_dict()
if is_state_dict_xlabs_controlnet(state_dict) or is_state_dict_instantx_controlnet(state_dict):
# TODO(ryand): Should I distinguish between XLabs, InstantX and other ControlNet models by implementing
# get_format()?
return BaseModelType.Flux
for key in (
"control_model.input_blocks.2.1.transformer_blocks.0.attn2.to_k.weight",
"controlnet_mid_block.bias",
"input_blocks.2.1.transformer_blocks.0.attn2.to_k.weight",
"down_blocks.1.attentions.0.transformer_blocks.0.attn2.to_k.weight",
):
if key not in state_dict:
continue
width = state_dict[key].shape[-1]
match width:
case 768:
return BaseModelType.StableDiffusion1
case 1024:
return BaseModelType.StableDiffusion2
case 2048:
return BaseModelType.StableDiffusionXL
case 1280:
return BaseModelType.StableDiffusionXL
case _:
pass
raise NotAMatchError("unable to determine base type from state dict")
class ControlNet_Checkpoint_SD1_Config(ControlNet_Checkpoint_Config_Base, Config_Base):
base: Literal[BaseModelType.StableDiffusion1] = Field(default=BaseModelType.StableDiffusion1)
class ControlNet_Checkpoint_SD2_Config(ControlNet_Checkpoint_Config_Base, Config_Base):
base: Literal[BaseModelType.StableDiffusion2] = Field(default=BaseModelType.StableDiffusion2)
class ControlNet_Checkpoint_SDXL_Config(ControlNet_Checkpoint_Config_Base, Config_Base):
base: Literal[BaseModelType.StableDiffusionXL] = Field(default=BaseModelType.StableDiffusionXL)
class ControlNet_Checkpoint_FLUX_Config(ControlNet_Checkpoint_Config_Base, Config_Base):
base: Literal[BaseModelType.Flux] = Field(default=BaseModelType.Flux)
def _has_z_image_control_keys(state_dict: dict) -> bool:
"""Check if state dict contains Z-Image Control specific keys."""
z_image_control_keys = {"control_layers", "control_all_x_embedder", "control_noise_refiner"}
for key in state_dict.keys():
if isinstance(key, str):
prefix = key.split(".")[0]
if prefix in z_image_control_keys:
return True
return False
class ControlNet_Checkpoint_ZImage_Config(Checkpoint_Config_Base, Config_Base):
"""Model config for Z-Image Control adapter models (Safetensors checkpoint).
Z-Image Control models are standalone adapters containing only the control layers
(control_layers, control_all_x_embedder, control_noise_refiner) that extend
the base Z-Image transformer with spatial conditioning capabilities.
Supports: Canny, HED, Depth, Pose, MLSD.
Recommended control_context_scale: 0.65-0.80.
"""
type: Literal[ModelType.ControlNet] = Field(default=ModelType.ControlNet)
format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint)
base: Literal[BaseModelType.ZImage] = Field(default=BaseModelType.ZImage)
default_settings: ControlAdapterDefaultSettings | None = Field(None)
@classmethod
def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self:
raise_if_not_file(mod)
raise_for_override_fields(cls, override_fields)
cls._validate_looks_like_z_image_control(mod)
return cls(**override_fields)
@classmethod
def _validate_looks_like_z_image_control(cls, mod: ModelOnDisk) -> None:
state_dict = mod.load_state_dict()
if not _has_z_image_control_keys(state_dict):
raise NotAMatchError("state dict does not look like a Z-Image Control model")
def _has_anima_lllite_keys(state_dict: dict) -> bool:
"""Check if state dict contains Anima ControlNet-LLLite specific keys.
Anima LLLite adapters (v2 named-key format) have a shared conditioning trunk under
`lllite_conditioning1.*` and per-module weights under `lllite_dit_blocks_*`. SDXL
ControlNet-LLLite models use `lllite_unet_*` keys instead and do not match.
"""
return state_dict_has_any_keys_starting_with(
state_dict, "lllite_conditioning1."
) and state_dict_has_any_keys_starting_with(state_dict, "lllite_dit_blocks_")
class ControlNet_Checkpoint_Anima_Config(Checkpoint_Config_Base, Config_Base):
"""Model config for Anima ControlNet-LLLite adapter models (Safetensors checkpoint).
Anima LLLite adapters are standalone adapters consisting of a shared conditioning trunk
(lllite_conditioning1) that encodes a conditioning image, plus tiny per-Linear modules
(lllite_dit_blocks_*) that perturb the inputs of target Linears in the Anima DiT.
"""
type: Literal[ModelType.ControlNet] = Field(default=ModelType.ControlNet)
format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint)
base: Literal[BaseModelType.Anima] = Field(default=BaseModelType.Anima)
default_settings: ControlAdapterDefaultSettings | None = Field(None)
cond_in_channels: int | None = Field(
default=None,
description="Number of conditioning image channels (3 = RGB control image, 4 = RGB + inpaint mask). None for "
"models installed before this field was recorded.",
)
@classmethod
def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self:
raise_if_not_file(mod)
raise_for_override_fields(cls, override_fields)
cls._validate_looks_like_anima_lllite(mod)
args = dict(override_fields)
if "cond_in_channels" not in args:
args["cond_in_channels"] = cls._get_cond_in_channels(mod)
return cls(**args)
@classmethod
def _get_cond_in_channels(cls, mod: ModelOnDisk) -> int:
# Mirrors AnimaControlNetLLLite.from_state_dict: prefer the saved `lllite.*` hyperparam, falling back to
# the conv1 weight shape (ch_half, cond_in_channels, 4, 4).
meta_value = mod.metadata().get("lllite.cond_in_channels")
if meta_value is not None:
return int(meta_value)
conv1_weight = mod.load_state_dict().get("lllite_conditioning1.conv1.weight")
if conv1_weight is None:
raise NotAMatchError("state dict has Anima ControlNet-LLLite keys but no lllite_conditioning1.conv1.weight")
return int(conv1_weight.shape[1])
@classmethod
def _validate_looks_like_anima_lllite(cls, mod: ModelOnDisk) -> None:
state_dict = mod.load_state_dict()
if not _has_anima_lllite_keys(state_dict):
raise NotAMatchError("state dict does not look like an Anima ControlNet-LLLite model")