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

258 lines
11 KiB
Python

from abc import ABC, abstractmethod
from enum import Enum
from inspect import isabstract
from typing import (
TYPE_CHECKING,
Any,
ClassVar,
Literal,
Self,
Type,
)
from pydantic import BaseModel, ConfigDict, Field, Tag, field_validator
from pydantic_core import PydanticUndefined
from invokeai.app.util.misc import uuid_string
from invokeai.backend.model_manager.model_on_disk import ModelOnDisk
from invokeai.backend.model_manager.taxonomy import (
AnyVariant,
BaseModelType,
ModelFormat,
ModelRepoVariant,
ModelSourceType,
ModelType,
)
if TYPE_CHECKING:
pass
class Config_Base(ABC, BaseModel):
"""
Abstract base class for model configurations. A model config describes a specific combination of model base, type and
format, along with other metadata about the model. For example, a Stable Diffusion 1.x main model in checkpoint format
would have base=sd-1, type=main, format=checkpoint.
To create a new config type, inherit from this class and implement its interface:
- Define method 'from_model_on_disk' that returns an instance of the class or raises NotAMatch. This method will be
called during model installation to determine the correct config class for a model.
- Define fields 'type', 'base' and 'format' as pydantic fields. These should be Literals with a single value. A
default must be provided for each of these fields.
If multiple combinations of base, type and format need to be supported, create a separate subclass for each.
See MinimalConfigExample in test_model_probe.py for an example implementation.
"""
# These fields are common to all model configs.
key: str = Field(
default_factory=uuid_string,
description="A unique key for this model.",
)
hash: str = Field(
description="The hash of the model file(s).",
)
path: str = Field(
description="Path to the model on the filesystem. Relative paths are relative to the Invoke root directory.",
)
file_size: int = Field(
description="The size of the model in bytes.",
)
name: str = Field(
description="Name of the model.",
)
description: str | None = Field(
default=None,
description="Model description",
)
source: str = Field(
description="The original source of the model (path, URL or repo_id).",
)
source_type: ModelSourceType = Field(
description="The type of source",
)
source_api_response: str | None = Field(
default=None,
description="The original API response from the source, as stringified JSON.",
)
source_url: str | None = Field(
default=None,
description="Optional URL for the model (e.g. download page or model page).",
)
@field_validator("source_url", mode="before")
@classmethod
def validate_source_url(cls, v: Any) -> str | None:
if v is None or v == "":
return None
if not isinstance(v, str):
raise ValueError("source_url must be a string")
if not v.startswith(("https://", "http://")):
raise ValueError("source_url must be an http or https URL")
return v
cover_image: str | None = Field(
default=None,
description="Url for image to preview model",
)
CONFIG_CLASSES: ClassVar[set[Type["Config_Base"]]] = set()
"""Set of all non-abstract subclasses of Config_Base, for use during model probing. In other words, this is the set
of all known model config types."""
model_config = ConfigDict(
validate_assignment=True,
json_schema_serialization_defaults_required=True,
json_schema_mode_override="serialization",
)
@classmethod
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
# Register non-abstract subclasses so we can iterate over them later during model probing. Note that
# isabstract() will return False if the class does not have any abstract methods, even if it inherits from ABC.
# We must check for ABC lest we unintentionally register some abstract model config classes.
if not isabstract(cls) and ABC not in cls.__bases__:
cls.CONFIG_CLASSES.add(cls)
@classmethod
def __pydantic_init_subclass__(cls, **kwargs):
# Ensure that model configs define 'base', 'type' and 'format' fields and provide defaults for them. Each
# subclass is expected to represent a single combination of base, type and format.
#
# This pydantic dunder method is called after the pydantic model for a class is created. The normal
# __init_subclass__ is too early to do this check.
for name in ("type", "base", "format"):
if name not in cls.model_fields:
raise NotImplementedError(f"{cls.__name__} must define a '{name}' field")
if cls.model_fields[name].default is PydanticUndefined:
raise NotImplementedError(f"{cls.__name__} must define a default for the '{name}' field")
@classmethod
def get_tag(cls) -> Tag:
"""Constructs a pydantic discriminated union tag for this model config class. When a config is deserialized,
pydantic uses the tag to determine which subclass to instantiate.
The tag is a dot-separated string of the type, format, base and variant (if applicable).
"""
tag_strings: list[str] = []
for name in ("type", "format", "base", "variant"):
if field := cls.model_fields.get(name):
# The check in __pydantic_init_subclass__ ensures that type, format and base are always present with
# defaults. variant does not require a default, but if it has one, we need to add it to the tag. We can
# check for the presence of a default by seeing if it's not PydanticUndefined, a sentinel value used by
# pydantic to indicate that no default was provided.
if field.default is not PydanticUndefined and field.default is not None:
# We expect each of these fields has an Enum for its default; we want the value of the enum.
tag_strings.append(field.default.value)
return Tag(".".join(tag_strings))
@staticmethod
def get_model_discriminator_value(v: Any) -> str:
"""Computes the discriminator value for a model config discriminated union."""
# This is called by pydantic during deserialization and serialization to determine which model the data
# represents. It can get either a dict (during deserialization) or an instance of a Config_Base subclass
# (during serialization).
#
# See: https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions-with-callable-discriminator
if isinstance(v, Config_Base):
# We have an instance of a ModelConfigBase subclass - use its tag directly.
return v.get_tag().tag
if isinstance(v, dict):
# We have a dict - attempt to compute a tag from its fields.
tag_strings: list[str] = []
if type_ := v.get("type"):
if isinstance(type_, Enum):
type_ = str(type_.value)
elif not isinstance(type_, str):
raise ValueError("Model config dict 'type' field must be a string or Enum")
tag_strings.append(type_)
if format_ := v.get("format"):
if isinstance(format_, Enum):
format_ = str(format_.value)
elif not isinstance(format_, str):
raise ValueError("Model config dict 'format' field must be a string or Enum")
tag_strings.append(format_)
if base_ := v.get("base"):
if isinstance(base_, Enum):
base_ = str(base_.value)
elif not isinstance(base_, str):
raise ValueError("Model config dict 'base' field must be a string or Enum")
tag_strings.append(base_)
# Special case: CLIP Embed models also need the variant to distinguish them.
if (
type_ == ModelType.CLIPEmbed.value
and format_ == ModelFormat.Diffusers.value
and base_ == BaseModelType.Any.value
):
if variant_ := v.get("variant"):
if isinstance(variant_, Enum):
variant_ = variant_.value
elif not isinstance(variant_, str):
raise ValueError("Model config dict 'variant' field must be a string or Enum")
tag_strings.append(variant_)
else:
raise ValueError("CLIP Embed model config dict must include a 'variant' field")
return ".".join(tag_strings)
else:
raise ValueError(
"Model config discriminator value must be computed from a dict or ModelConfigBase instance"
)
@classmethod
@abstractmethod
def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self:
"""Given the model on disk and any override fields, attempt to construct an instance of this config class.
This method serves to identify whether the model on disk matches this config class, and if so, to extract any
additional metadata needed to instantiate the config.
Implementations should raise a NotAMatchError if the model does not match this config class."""
raise NotImplementedError(f"from_model_on_disk not implemented for {cls.__name__}")
class Checkpoint_Config_Base(ABC, BaseModel):
"""Base class for checkpoint-style models."""
config_path: str | None = Field(
description="Path to the config for this model, if any.",
default=None,
)
class Diffusers_Config_Base(ABC, BaseModel):
"""Base class for diffusers-style models."""
format: Literal[ModelFormat.Diffusers] = Field(default=ModelFormat.Diffusers)
repo_variant: ModelRepoVariant = Field(ModelRepoVariant.Default)
@classmethod
def _get_repo_variant_or_raise(cls, mod: ModelOnDisk) -> ModelRepoVariant:
# get all files ending in .bin or .safetensors
weight_files = list(mod.path.glob("**/*.safetensors"))
weight_files.extend(list(mod.path.glob("**/*.bin")))
for x in weight_files:
if ".fp16" in x.suffixes:
return ModelRepoVariant.FP16
if "openvino_model" in x.name:
return ModelRepoVariant.OpenVINO
if "flax_model" in x.name:
return ModelRepoVariant.Flax
if x.suffix == ".onnx":
return ModelRepoVariant.ONNX
return ModelRepoVariant.Default
class SubmodelDefinition(BaseModel):
path_or_prefix: str
model_type: ModelType
variant: AnyVariant | None = None
model_config = ConfigDict(protected_namespaces=())