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

207 lines
8.0 KiB
Python

import json
from functools import cache
from pathlib import Path
from pydantic import BaseModel, ValidationError
from pydantic_core import CoreSchema, SchemaValidator
from typing_extensions import Any
from invokeai.backend.model_manager.model_on_disk import ModelOnDisk
class NotAMatchError(Exception):
"""Exception for when a model does not match a config class.
Args:
reason: The reason why the model did not match.
"""
def __init__(self, reason: str):
super().__init__(reason)
def get_config_dict_or_raise(config_path: Path | set[Path]) -> dict[str, Any]:
"""Load the diffusers/transformers model config file and return it as a dictionary. The config file is expected
to be in JSON format.
Args:
config_path: The path to the config file, or a set of paths to try.
Returns:
The config file as a dictionary.
Raises:
NotAMatch if the config file is missing or cannot be loaded.
"""
paths_to_check = config_path if isinstance(config_path, set) else {config_path}
problems: dict[Path, str] = {}
for p in paths_to_check:
if not p.exists():
problems[p] = "file does not exist"
continue
try:
with open(p, "r") as file:
config = json.load(file)
return config
except Exception as e:
problems[p] = str(e)
continue
raise NotAMatchError(f"unable to load config file(s): {problems}")
def get_class_name_from_config_dict_or_raise(config: Path | set[Path] | dict[str, Any]) -> str:
"""Load the diffusers/transformers model config file and return the class name.
Args:
config_path: The path to the config file, or a set of paths to try.
Returns:
The class name from the config file.
Raises:
NotAMatch if the config file is missing or does not contain a valid class name.
"""
if not isinstance(config, dict):
config = get_config_dict_or_raise(config)
try:
if "_class_name" in config:
# This is a diffusers-style config
config_class_name = config["_class_name"]
elif "architectures" in config:
# This is a transformers-style config
config_class_name = config["architectures"][0]
else:
raise ValueError("missing _class_name or architectures field")
except Exception as e:
raise NotAMatchError(f"unable to determine class name from config file: {config}") from e
if not isinstance(config_class_name, str):
raise NotAMatchError(f"_class_name or architectures field is not a string: {config_class_name}")
return config_class_name
def raise_for_class_name(config: Path | set[Path] | dict[str, Any], class_name: str | set[str]) -> None:
"""Get the class name from the config file and raise NotAMatch if it is not in the expected set.
Args:
config_path: The path to the config file, or a set of paths to try.
class_name: The expected class name, or a set of expected class names.
Raises:
NotAMatch if the class name is not in the expected set.
"""
class_name = {class_name} if isinstance(class_name, str) else class_name
actual_class_name = get_class_name_from_config_dict_or_raise(config)
if actual_class_name not in class_name:
raise NotAMatchError(f"invalid class name from config: {actual_class_name}")
def raise_for_override_fields(candidate_config_class: type[BaseModel], override_fields: dict[str, Any]) -> None:
"""Check if the provided override fields are valid for the config class using pydantic.
For example, if the candidate config class has a field "base" of type Literal[BaseModelType.StableDiffusion1], and
the override fields contain "base": BaseModelType.Flux, this function will raise NotAMatch.
Internally, this function extracts the pydantic schema for each individual override field from the candidate config
class and validates the override value against that schema. Post-instantiation validators are not run.
Args:
candidate_config_class: The config class that is being tested.
override_fields: The override fields provided by the user.
Raises:
NotAMatch if any override field is invalid for the config class.
"""
for field_name, override_value in override_fields.items():
if field_name not in candidate_config_class.model_fields:
raise NotAMatchError(f"unknown override field: {field_name}")
try:
PydanticFieldValidator.validate_field(candidate_config_class, field_name, override_value)
except ValidationError as e:
raise NotAMatchError(f"invalid override for field '{field_name}': {e}") from e
def raise_if_not_file(mod: ModelOnDisk) -> None:
"""Raise NotAMatch if the model path is not a file."""
if not mod.path.is_file():
raise NotAMatchError("model path is not a file")
def raise_if_not_dir(mod: ModelOnDisk) -> None:
"""Raise NotAMatch if the model path is not a directory."""
if not mod.path.is_dir():
raise NotAMatchError("model path is not a directory")
def state_dict_has_any_keys_exact(state_dict: dict[str | int, Any], keys: str | set[str]) -> bool:
"""Returns true if the state dict has any of the specified keys."""
_keys = {keys} if isinstance(keys, str) else keys
return any(key in state_dict for key in _keys)
def state_dict_has_any_keys_starting_with(state_dict: dict[str | int, Any], prefixes: str | set[str]) -> bool:
"""Returns true if the state dict has any keys starting with any of the specified prefixes."""
_prefixes = {prefixes} if isinstance(prefixes, str) else prefixes
return any(any(key.startswith(prefix) for prefix in _prefixes) for key in state_dict.keys() if isinstance(key, str))
def state_dict_has_any_keys_ending_with(state_dict: dict[str | int, Any], suffixes: str | set[str]) -> bool:
"""Returns true if the state dict has any keys ending with any of the specified suffixes."""
_suffixes = {suffixes} if isinstance(suffixes, str) else suffixes
return any(any(key.endswith(suffix) for suffix in _suffixes) for key in state_dict.keys() if isinstance(key, str))
def common_config_paths(path: Path) -> set[Path]:
"""Returns common config file paths for models stored in directories."""
return {path / "config.json", path / "model_index.json"}
class PydanticFieldValidator:
"""Utility class for validating individual fields of a Pydantic model without instantiating the whole model.
See: https://github.com/pydantic/pydantic/discussions/7367#discussioncomment-14213144
"""
@staticmethod
def find_field_schema(model: type[BaseModel], field_name: str) -> CoreSchema:
"""Find the Pydantic core schema for a specific field in a model."""
schema: CoreSchema = model.__pydantic_core_schema__.copy()
# we shallow copied, be careful not to mutate the original schema!
assert schema["type"] in ["definitions", "model"]
# find the field schema
field_schema = schema["schema"] # type: ignore
while "fields" not in field_schema:
field_schema = field_schema["schema"] # type: ignore
field_schema = field_schema["fields"][field_name]["schema"] # type: ignore
# if the original schema is a definition schema, replace the model schema with the field schema
if schema["type"] == "definitions":
schema["schema"] = field_schema
return schema
else:
return field_schema
@cache
@staticmethod
def get_validator(model: type[BaseModel], field_name: str) -> SchemaValidator:
"""Get a SchemaValidator for a specific field in a model."""
return SchemaValidator(PydanticFieldValidator.find_field_schema(model, field_name))
@staticmethod
def validate_field(model: type[BaseModel], field_name: str, value: Any) -> Any:
"""Validate a value for a specific field in a model."""
return PydanticFieldValidator.get_validator(model, field_name).validate_python(value)