From 742ed19d661094dbaddc1dbeccef5a021bc7dca1 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Thu, 4 May 2023 01:20:30 -0400 Subject: [PATCH] add missing config module --- invokeai/app/services/config.py | 423 ++++++++++++++++++++++++++++++++ 1 file changed, 423 insertions(+) create mode 100644 invokeai/app/services/config.py diff --git a/invokeai/app/services/config.py b/invokeai/app/services/config.py new file mode 100644 index 0000000000..3fcbc4e1c3 --- /dev/null +++ b/invokeai/app/services/config.py @@ -0,0 +1,423 @@ +# Copyright (c) 2023 Lincoln Stein (https://github.com/lstein) + +'''Invokeai configuration system. + +Arguments and fields are taken from the pydantic definition of the +model. Defaults can be set by creating a yaml configuration file that +has top-level keys corresponding to an invocation name, a command, or +"globals" for global values such as `xformers_enabled`. Currently +graphs cannot be configured this way, but their constituents can be. + +[file: invokeai.yaml] + + globals: + nsfw_checker: False + max_loaded_models: 5 + + txt2img: + steps: 20 + scheduler: k_heun + width: 768 + + img2img: + width: 1024 + height: 1024 + +The default name of the configuration file is `invokeai.yaml`, located +in INVOKEAI_ROOT. You can use any OmegaConf dictionary by passing it +to the config object at initialization time: + + omegaconf = OmegaConf.load('/tmp/init.yaml') + conf = InvokeAIAppConfig(conf=omegaconf) + +By default, InvokeAIAppConfig will parse the contents of argv at +initialization time. You may pass a list of strings in the optional +`argv` argument to use instead of the system argv: + + conf = InvokeAIAppConfig(arg=['--xformers_enabled']) + +It is also possible to set a value at initialization time. This value +has highest priority. + + conf = InvokeAIAppConfig(xformers_enabled=True) + +Any setting can be overwritten by setting an environment variable of +form: "INVOKEAI__", as in: + + export INVOKEAI_txt2img_steps=30 + +Order of precedence (from highest): + 1) initialization options + 2) command line options + 3) environment variable options + 4) config file options + 5) pydantic defaults + +Typical usage: + + from invokeai.app.services.config import InvokeAIAppConfig + from invokeai.invocations.generate import TextToImageInvocation + + # get global configuration and print its nsfw_checker value + conf = InvokeAIAppConfig() + print(conf.nsfw_checker) + + # get the text2image invocation and print its step value + text2image = TextToImageInvocation() + print(text2image.steps) + +Computed properties: + +The InvokeAIAppConfig object has a series of properties that +resolve paths relative to the runtime root directory. They each return +a Path object: + + root_path - path to InvokeAI root + output_path - path to default outputs directory + model_conf_path - path to models.yaml + conf - alias for the above + embedding_path - path to the embeddings directory + lora_path - path to the LoRA directory + +In most cases, you will want to create a single InvokeAIAppConfig +object for the entire application. The get_invokeai_config() function +does this: + + config = get_invokeai_config() + print(config.root) + +''' +import argparse +import os +import sys +from argparse import ArgumentParser +from omegaconf import OmegaConf, DictConfig +from pathlib import Path +from pydantic import BaseSettings, Field, parse_obj_as +from typing import Any, ClassVar, Dict, List, Literal, Type, Union, get_origin, get_type_hints, get_args + +INIT_FILE = Path('invokeai.yaml') +LEGACY_INIT_FILE = Path('invokeai.init') + +# This global stores a singleton InvokeAIAppConfig configuration object +global_config = None + +class InvokeAISettings(BaseSettings): + ''' + Runtime configuration settings in which default values are + read from an omegaconf .yaml file. + ''' + initconf : ClassVar[DictConfig] = None + argparse_groups : ClassVar[Dict] = {} + + def parse_args(self, argv: list=sys.argv[1:]): + parser = self.get_parser() + opt, _ = parser.parse_known_args(argv) + for name in self.__fields__: + if name not in self._excluded(): + setattr(self, name, getattr(opt,name)) + + @classmethod + def add_parser_arguments(cls, parser): + env_prefix = cls.Config.env_prefix if hasattr(cls.Config,'env_prefix') else 'INVOKEAI_' + if 'type' in get_type_hints(cls): + default_settings_stanza = get_args(get_type_hints(cls)['type'])[0] + else: + default_settings_stanza = 'globals' + initconf = cls.initconf.get(default_settings_stanza) if cls.initconf and default_settings_stanza in cls.initconf else None + + fields = cls.__fields__ + cls.argparse_groups = {} + for name, field in fields.items(): + if name not in cls._excluded(): + env_name = env_prefix+f'{cls.cmd_name()}_{name}' + if initconf and name in initconf: + field.default = initconf.get(name) + if env_name in os.environ: + field.default = os.environ[env_name] + cls.add_field_argument(parser, name, field) + + + @classmethod + def cmd_name(self, command_field: str='type')->str: + hints = get_type_hints(self) + if command_field in hints: + return get_args(hints[command_field])[0] + else: + return 'globals' + + @classmethod + def get_parser(cls)->ArgumentParser: + parser = ArgumentParser( + prog=cls.cmd_name(), + description=cls.__doc__, + ) + cls.add_parser_arguments(parser) + return parser + + @classmethod + def add_subparser(cls, parser: argparse.ArgumentParser): + parser.add_parser(cls.cmd_name(), help=cls.__doc__) + + @classmethod + def _excluded(self)->List[str]: + return ['type','initconf'] + + class Config: + env_file_encoding = 'utf-8' + arbitrary_types_allowed = True + env_prefix = 'INVOKEAI_' + case_sensitive = True + @classmethod + def customise_sources( + cls, + init_settings, + env_settings, + file_secret_settings, + ): + return ( + init_settings, + cls._omegaconf_settings_source, + env_settings, + file_secret_settings, + ) + + @classmethod + def _omegaconf_settings_source(cls, settings: BaseSettings) -> dict[str, Any]: + if initconf := InvokeAISettings.initconf: + return initconf.get(settings.cmd_name(),{}) + else: + return {} + + @classmethod + def add_field_argument(cls, command_parser, name: str, field, default_override = None): + default = default_override if default_override is not None else field.default if field.default_factory is None else field.default_factory() + if category := field.field_info.extra.get("category"): + if category not in cls.argparse_groups: + cls.argparse_groups[category] = command_parser.add_argument_group(category) + argparse_group = cls.argparse_groups[category] + else: + argparse_group = command_parser + + if get_origin(field.type_) == Literal: + allowed_values = get_args(field.type_) + allowed_types = set() + for val in allowed_values: + allowed_types.add(type(val)) + allowed_types_list = list(allowed_types) + field_type = allowed_types_list[0] if len(allowed_types) == 1 else Union[allowed_types_list] # type: ignore + + argparse_group.add_argument( + f"--{name}", + dest=name, + type=field_type, + default=default, + choices=allowed_values, + help=field.field_info.description, + ) + else: + argparse_group.add_argument( + f"--{name}", + dest=name, + type=field.type_, + default=default, + action=argparse.BooleanOptionalAction if field.type_==bool else 'store', + help=field.field_info.description, + ) +def _find_root()->Path: + if os.environ.get("INVOKEAI_ROOT"): + root = Path(os.environ.get("INVOKEAI_ROOT")).resolve() + elif ( + os.environ.get("VIRTUAL_ENV") + and (Path(os.environ.get("VIRTUAL_ENV"), "..", INIT_FILE).exists() + or + Path(os.environ.get("VIRTUAL_ENV"), "..", LEGACY_INIT_FILE).exists() + ) + ): + root = Path(os.environ.get("VIRTUAL_ENV"), "..").resolve() + else: + root = Path("~/invokeai").expanduser().resolve() + return root + +class InvokeAIAppConfig(InvokeAISettings): + ''' + Application-wide settings. + ''' + #fmt: off + type: Literal["globals"] = "globals" + root : Path = Field(default=_find_root(), description='InvokeAI runtime root directory', category='Paths') + conf_path : Path = Field(default='configs/models.yaml', description='Path to models definition file', category='Paths') + legacy_conf_dir : Path = Field(default='configs/stable-diffusion', description='Path to directory of legacy checkpoint config files', category='Paths') + model : str = Field(default='stable-diffusion-1.5', description='Initial model name', category='Models') + outdir : Path = Field(default='outputs', description='Default folder for output images', category='Paths') + embedding_dir : Path = Field(default='embeddings', description='Path to InvokeAI textual inversion aembeddings directory', category='Paths') + lora_dir : Path = Field(default='loras', description='Path to InvokeAI LoRA model directory', category='Paths') + autoconvert_dir : Path = Field(default=None, description='Path to a directory of ckpt files to be converted into diffusers and imported on startup.', category='Paths') + gfpgan_model_dir : Path = Field(default="./models/gfpgan/GFPGANv1.4.pth", description='Path to GFPGAN models directory.', category='Paths') + embeddings : bool = Field(default=True, description='Load contents of embeddings directory', category='Models') + xformers_enabled : bool = Field(default=True, description="Enable/disable memory-efficient attention", category='Memory/Performance') + sequential_guidance : bool = Field(default=False, description="Whether to calculate guidance in serial instead of in parallel, lowering memory requirements", category='Memory/Performance') + precision : Literal[tuple(['auto','float16','float32','autocast'])] = Field(default='float16',description='Floating point precision', category='Memory/Performance') + max_loaded_models : int = Field(default=2, gt=0, description="Maximum number of models to keep in memory for rapid switching", category='Memory/Performance') + always_use_cpu : bool = Field(default=False, description="If true, use the CPU for rendering even if a GPU is available.", category='Memory/Performance') + free_gpu_mem : bool = Field(default=False, description="If true, purge model from GPU after each generation.", category='Memory/Performance') + nsfw_checker : bool = Field(default=True, description="Enable/disable the NSFW checker", category='Features') + restore : bool = Field(default=True, description="Enable/disable face restoration code", category='Features') + esrgan : bool = Field(default=True, description="Enable/disable upscaling code", category='Features') + patchmatch : bool = Field(default=True, description="Enable/disable patchmatch inpaint code", category='Features') + internet_available : bool = Field(default=True, description="If true, attempt to download models on the fly; otherwise only use local models", category='Features') + log_tokenization : bool = Field(default=False, description="Enable logging of parsed prompt tokens.", category='Features') + #fmt: on + + def __init__(self, conf: DictConfig = None, argv: List[str]=None, **kwargs): + ''' + Initialize InvokeAIAppconfig. + :param conf: alternate Omegaconf dictionary object + :param argv: aternate sys.argv list + :param **kwargs: attributes to initialize with + ''' + super().__init__(**kwargs) + + # Set the runtime root directory. We parse command-line switches here + # in order to pick up the --root_dir option. + self.parse_args(argv) + if not conf: + try: + conf = OmegaConf.load(self.root_dir / INIT_FILE) + except: + pass + InvokeAISettings.initconf = conf + + # parse args again in order to pick up settings in configuration file + self.parse_args(argv) + + # restore initialization values + hints = get_type_hints(self) + for k in kwargs: + setattr(self,k,parse_obj_as(hints[k],kwargs[k])) + + @property + def root_path(self)->Path: + ''' + Path to the runtime root directory + ''' + if self.root: + return Path(self.root).expanduser() + else: + return self.find_root() + + @property + def root_dir(self)->Path: + ''' + Alias for above. + ''' + return self.root_path + + def _resolve(self,partial_path:Path)->Path: + return (self.root_path / partial_path).resolve() + + @property + def output_path(self)->Path: + ''' + Path to defaults outputs directory. + ''' + return self._resolve(self.outdir) + + @property + def model_conf_path(self)->Path: + ''' + Path to models configuration file. + ''' + return self._resolve(self.conf_path) + + @property + def legacy_conf_path(self)->Path: + ''' + Path to directory of legacy configuration files (e.g. v1-inference.yaml) + ''' + return self._resolve(self.legacy_conf_dir) + + @property + def cache_dir(self)->Path: + ''' + Path to the global cache directory for HuggingFace hub-managed models + ''' + return self.models_dir / "hub" + + @property + def models_dir(self)->Path: + ''' + Path to the models directory + ''' + return self._resolve("models") + + @property + def embedding_path(self)->Path: + ''' + Path to the textual inversion embeddings directory. + ''' + return self._resolve(self.embedding_dir) if self.embedding_dir else None + + @property + def lora_path(self)->Path: + ''' + Path to the LoRA models directory. + ''' + return self._resolve(self.lora_dir) if self.lora_dir else None + + @property + def autoconvert_path(self)->Path: + ''' + Path to the directory containing models to be imported automatically at startup. + ''' + return self._resolve(self.autoconvert_dir) if self.autoconvert_dir else None + + @property + def gfpgan_model_path(self)->Path: + ''' + Path to the GFPGAN model. + ''' + return self._resolve(self.gfpgan_model_dir) if self.gfpgan_model_dir else None + + # the following methods support legacy calls leftover from the Globals era + @property + def full_precision(self)->bool: + """Return true if precision set to float32""" + return self.precision=='float32' + + @property + def disable_xformers(self)->bool: + """Return true if xformers_enabled is false""" + return not self.xformers_enabled + + @staticmethod + def find_root()->Path: + ''' + Choose the runtime root directory when not specified on command line or + init file. + ''' + return _find_root() + +class InvokeAIWebConfig(InvokeAIAppConfig): + ''' + Web-specific settings + ''' + #fmt: off + type : Literal["web"] = "web" + allow_origins : List = Field(default=[], description="Allowed CORS origins", category='Cross-Origin Resource Sharing') + allow_credentials : bool = Field(default=True, description="Allow CORS credentials", category='Cross-Origin Resource Sharing') + allow_methods : List = Field(default=["*"], description="Methods allowed for CORS", category='Cross-Origin Resource Sharing') + allow_headers : List = Field(default=["*"], description="Headers allowed for CORS", category='Cross-Origin Resource Sharing') + host : str = Field(default="127.0.0.1", description="IP address to bind to", category='Web Server') + port : int = Field(default=9090, description="Port to bind to", category='Web Server') + #fmt: on + + +def get_invokeai_config(cls:Type[InvokeAISettings]=InvokeAIAppConfig)->InvokeAISettings: + ''' + This returns a singleton InvokeAIAppConfig configuration object. + ''' + global global_config + if global_config is None or type(global_config)!=cls: + global_config = cls() + return global_config