CoolFace
Apppublic

Allex21/LT

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
config_util.py722 linesDownload Raw Back to sd-scripts
1import argparse2from dataclasses import (3    asdict,4    dataclass,5)6import functools7import random8from textwrap import dedent, indent9import json10from pathlib import Path11 12# from toolz import curry13from typing import (14    List,15    Optional,16    Sequence,17    Tuple,18    Union,19)20 21import toml22import voluptuous23from voluptuous import (24    Any,25    ExactSequence,26    MultipleInvalid,27    Object,28    Required,29    Schema,30)31from transformers import CLIPTokenizer32 33from . import train_util34from .train_util import (35    DreamBoothSubset,36    FineTuningSubset,37    ControlNetSubset,38    DreamBoothDataset,39    FineTuningDataset,40    ControlNetDataset,41    DatasetGroup,42)43from .utils import setup_logging44 45setup_logging()46import logging47 48logger = logging.getLogger(__name__)49 50 51def add_config_arguments(parser: argparse.ArgumentParser):52    parser.add_argument(53        "--dataset_config", type=Path, default=None, help="config file for detail settings / 詳細な設定用の設定ファイル"54    )55 56 57# TODO: inherit Params class in Subset, Dataset58 59 60@dataclass61class BaseSubsetParams:62    image_dir: Optional[str] = None63    num_repeats: int = 164    shuffle_caption: bool = False65    caption_separator: str = (",",)66    keep_tokens: int = 067    keep_tokens_separator: str = (None,)68    secondary_separator: Optional[str] = None69    enable_wildcard: bool = False70    color_aug: bool = False71    flip_aug: bool = False72    face_crop_aug_range: Optional[Tuple[float, float]] = None73    random_crop: bool = False74    caption_prefix: Optional[str] = None75    caption_suffix: Optional[str] = None76    caption_dropout_rate: float = 0.077    caption_dropout_every_n_epochs: int = 078    caption_tag_dropout_rate: float = 0.079    token_warmup_min: int = 180    token_warmup_step: float = 081 82 83@dataclass84class DreamBoothSubsetParams(BaseSubsetParams):85    is_reg: bool = False86    class_tokens: Optional[str] = None87    caption_extension: str = ".caption"88    cache_info: bool = False89    alpha_mask: bool = False90 91 92@dataclass93class FineTuningSubsetParams(BaseSubsetParams):94    metadata_file: Optional[str] = None95    alpha_mask: bool = False96 97 98@dataclass99class ControlNetSubsetParams(BaseSubsetParams):100    conditioning_data_dir: str = None101    caption_extension: str = ".caption"102    cache_info: bool = False103 104 105@dataclass106class BaseDatasetParams:107    tokenizer: Union[CLIPTokenizer, List[CLIPTokenizer]] = None108    max_token_length: int = None109    resolution: Optional[Tuple[int, int]] = None110    network_multiplier: float = 1.0111    debug_dataset: bool = False112 113 114@dataclass115class DreamBoothDatasetParams(BaseDatasetParams):116    batch_size: int = 1117    enable_bucket: bool = False118    min_bucket_reso: int = 256119    max_bucket_reso: int = 1024120    bucket_reso_steps: int = 64121    bucket_no_upscale: bool = False122    prior_loss_weight: float = 1.0123 124 125@dataclass126class FineTuningDatasetParams(BaseDatasetParams):127    batch_size: int = 1128    enable_bucket: bool = False129    min_bucket_reso: int = 256130    max_bucket_reso: int = 1024131    bucket_reso_steps: int = 64132    bucket_no_upscale: bool = False133 134 135@dataclass136class ControlNetDatasetParams(BaseDatasetParams):137    batch_size: int = 1138    enable_bucket: bool = False139    min_bucket_reso: int = 256140    max_bucket_reso: int = 1024141    bucket_reso_steps: int = 64142    bucket_no_upscale: bool = False143 144 145@dataclass146class SubsetBlueprint:147    params: Union[DreamBoothSubsetParams, FineTuningSubsetParams]148 149 150@dataclass151class DatasetBlueprint:152    is_dreambooth: bool153    is_controlnet: bool154    params: Union[DreamBoothDatasetParams, FineTuningDatasetParams]155    subsets: Sequence[SubsetBlueprint]156 157 158@dataclass159class DatasetGroupBlueprint:160    datasets: Sequence[DatasetBlueprint]161 162 163@dataclass164class Blueprint:165    dataset_group: DatasetGroupBlueprint166 167 168class ConfigSanitizer:169    # @curry170    @staticmethod171    def __validate_and_convert_twodim(klass, value: Sequence) -> Tuple:172        Schema(ExactSequence([klass, klass]))(value)173        return tuple(value)174 175    # @curry176    @staticmethod177    def __validate_and_convert_scalar_or_twodim(klass, value: Union[float, Sequence]) -> Tuple:178        Schema(Any(klass, ExactSequence([klass, klass])))(value)179        try:180            Schema(klass)(value)181            return (value, value)182        except:183            return ConfigSanitizer.__validate_and_convert_twodim(klass, value)184 185    # subset schema186    SUBSET_ASCENDABLE_SCHEMA = {187        "color_aug": bool,188        "face_crop_aug_range": functools.partial(__validate_and_convert_twodim.__func__, float),189        "flip_aug": bool,190        "num_repeats": int,191        "random_crop": bool,192        "shuffle_caption": bool,193        "keep_tokens": int,194        "keep_tokens_separator": str,195        "secondary_separator": str,196        "caption_separator": str,197        "enable_wildcard": bool,198        "token_warmup_min": int,199        "token_warmup_step": Any(float, int),200        "caption_prefix": str,201        "caption_suffix": str,202    }203    # DO means DropOut204    DO_SUBSET_ASCENDABLE_SCHEMA = {205        "caption_dropout_every_n_epochs": int,206        "caption_dropout_rate": Any(float, int),207        "caption_tag_dropout_rate": Any(float, int),208    }209    # DB means DreamBooth210    DB_SUBSET_ASCENDABLE_SCHEMA = {211        "caption_extension": str,212        "class_tokens": str,213        "cache_info": bool,214    }215    DB_SUBSET_DISTINCT_SCHEMA = {216        Required("image_dir"): str,217        "is_reg": bool,218        "alpha_mask": bool,219    }220    # FT means FineTuning221    FT_SUBSET_DISTINCT_SCHEMA = {222        Required("metadata_file"): str,223        "image_dir": str,224        "alpha_mask": bool,225    }226    CN_SUBSET_ASCENDABLE_SCHEMA = {227        "caption_extension": str,228        "cache_info": bool,229    }230    CN_SUBSET_DISTINCT_SCHEMA = {231        Required("image_dir"): str,232        Required("conditioning_data_dir"): str,233    }234 235    # datasets schema236    DATASET_ASCENDABLE_SCHEMA = {237        "batch_size": int,238        "bucket_no_upscale": bool,239        "bucket_reso_steps": int,240        "enable_bucket": bool,241        "max_bucket_reso": int,242        "min_bucket_reso": int,243        "resolution": functools.partial(__validate_and_convert_scalar_or_twodim.__func__, int),244        "network_multiplier": float,245    }246 247    # options handled by argparse but not handled by user config248    ARGPARSE_SPECIFIC_SCHEMA = {249        "debug_dataset": bool,250        "max_token_length": Any(None, int),251        "prior_loss_weight": Any(float, int),252    }253    # for handling default None value of argparse254    ARGPARSE_NULLABLE_OPTNAMES = [255        "face_crop_aug_range",256        "resolution",257    ]258    # prepare map because option name may differ among argparse and user config259    ARGPARSE_OPTNAME_TO_CONFIG_OPTNAME = {260        "train_batch_size": "batch_size",261        "dataset_repeats": "num_repeats",262    }263 264    def __init__(self, support_dreambooth: bool, support_finetuning: bool, support_controlnet: bool, support_dropout: bool) -> None:265        assert support_dreambooth or support_finetuning or support_controlnet, (266            "Neither DreamBooth mode nor fine tuning mode nor controlnet mode specified. Please specify one mode or more."267            + " / DreamBooth モードか fine tuning モードか controlnet モードのどれも指定されていません。1つ以上指定してください。"268        )269 270        self.db_subset_schema = self.__merge_dict(271            self.SUBSET_ASCENDABLE_SCHEMA,272            self.DB_SUBSET_DISTINCT_SCHEMA,273            self.DB_SUBSET_ASCENDABLE_SCHEMA,274            self.DO_SUBSET_ASCENDABLE_SCHEMA if support_dropout else {},275        )276 277        self.ft_subset_schema = self.__merge_dict(278            self.SUBSET_ASCENDABLE_SCHEMA,279            self.FT_SUBSET_DISTINCT_SCHEMA,280            self.DO_SUBSET_ASCENDABLE_SCHEMA if support_dropout else {},281        )282 283        self.cn_subset_schema = self.__merge_dict(284            self.SUBSET_ASCENDABLE_SCHEMA,285            self.CN_SUBSET_DISTINCT_SCHEMA,286            self.CN_SUBSET_ASCENDABLE_SCHEMA,287            self.DO_SUBSET_ASCENDABLE_SCHEMA if support_dropout else {},288        )289 290        self.db_dataset_schema = self.__merge_dict(291            self.DATASET_ASCENDABLE_SCHEMA,292            self.SUBSET_ASCENDABLE_SCHEMA,293            self.DB_SUBSET_ASCENDABLE_SCHEMA,294            self.DO_SUBSET_ASCENDABLE_SCHEMA if support_dropout else {},295            {"subsets": [self.db_subset_schema]},296        )297 298        self.ft_dataset_schema = self.__merge_dict(299            self.DATASET_ASCENDABLE_SCHEMA,300            self.SUBSET_ASCENDABLE_SCHEMA,301            self.DO_SUBSET_ASCENDABLE_SCHEMA if support_dropout else {},302            {"subsets": [self.ft_subset_schema]},303        )304 305        self.cn_dataset_schema = self.__merge_dict(306            self.DATASET_ASCENDABLE_SCHEMA,307            self.SUBSET_ASCENDABLE_SCHEMA,308            self.CN_SUBSET_ASCENDABLE_SCHEMA,309            self.DO_SUBSET_ASCENDABLE_SCHEMA if support_dropout else {},310            {"subsets": [self.cn_subset_schema]},311        )312 313        if support_dreambooth and support_finetuning:314 315            def validate_flex_dataset(dataset_config: dict):316                subsets_config = dataset_config.get("subsets", [])317 318                if support_controlnet and all(["conditioning_data_dir" in subset for subset in subsets_config]):319                    return Schema(self.cn_dataset_schema)(dataset_config)320                # check dataset meets FT style321                # NOTE: all FT subsets should have "metadata_file"322                elif all(["metadata_file" in subset for subset in subsets_config]):323                    return Schema(self.ft_dataset_schema)(dataset_config)324                # check dataset meets DB style325                # NOTE: all DB subsets should have no "metadata_file"326                elif all(["metadata_file" not in subset for subset in subsets_config]):327                    return Schema(self.db_dataset_schema)(dataset_config)328                else:329                    raise voluptuous.Invalid(330                        "DreamBooth subset and fine tuning subset cannot be mixed in the same dataset. Please split them into separate datasets. / DreamBoothのサブセットとfine tuninのサブセットを同一のデータセットに混在させることはできません。別々のデータセットに分割してください。"331                    )332 333            self.dataset_schema = validate_flex_dataset334        elif support_dreambooth:335            if support_controlnet:336                self.dataset_schema = self.cn_dataset_schema337            else:338                self.dataset_schema = self.db_dataset_schema339        elif support_finetuning:340            self.dataset_schema = self.ft_dataset_schema341        elif support_controlnet:342            self.dataset_schema = self.cn_dataset_schema343 344        self.general_schema = self.__merge_dict(345            self.DATASET_ASCENDABLE_SCHEMA,346            self.SUBSET_ASCENDABLE_SCHEMA,347            self.DB_SUBSET_ASCENDABLE_SCHEMA if support_dreambooth else {},348            self.CN_SUBSET_ASCENDABLE_SCHEMA if support_controlnet else {},349            self.DO_SUBSET_ASCENDABLE_SCHEMA if support_dropout else {},350        )351 352        self.user_config_validator = Schema(353            {354                "general": self.general_schema,355                "datasets": [self.dataset_schema],356            }357        )358 359        self.argparse_schema = self.__merge_dict(360            self.general_schema,361            self.ARGPARSE_SPECIFIC_SCHEMA,362            {optname: Any(None, self.general_schema[optname]) for optname in self.ARGPARSE_NULLABLE_OPTNAMES},363            {a_name: self.general_schema[c_name] for a_name, c_name in self.ARGPARSE_OPTNAME_TO_CONFIG_OPTNAME.items()},364        )365 366        self.argparse_config_validator = Schema(Object(self.argparse_schema), extra=voluptuous.ALLOW_EXTRA)367 368    def sanitize_user_config(self, user_config: dict) -> dict:369        try:370            return self.user_config_validator(user_config)371        except MultipleInvalid:372            # TODO: エラー発生時のメッセージをわかりやすくする373            logger.error("Invalid user config / ユーザ設定の形式が正しくないようです")374            raise375 376    # NOTE: In nature, argument parser result is not needed to be sanitize377    #   However this will help us to detect program bug378    def sanitize_argparse_namespace(self, argparse_namespace: argparse.Namespace) -> argparse.Namespace:379        try:380            return self.argparse_config_validator(argparse_namespace)381        except MultipleInvalid:382            # XXX: this should be a bug383            logger.error(384                "Invalid cmdline parsed arguments. This should be a bug. / コマンドラインのパース結果が正しくないようです。プログラムのバグの可能性が高いです。"385            )386            raise387 388    # NOTE: value would be overwritten by latter dict if there is already the same key389    @staticmethod390    def __merge_dict(*dict_list: dict) -> dict:391        merged = {}392        for schema in dict_list:393            # merged |= schema394            for k, v in schema.items():395                merged[k] = v396        return merged397 398 399class BlueprintGenerator:400    BLUEPRINT_PARAM_NAME_TO_CONFIG_OPTNAME = {}401 402    def __init__(self, sanitizer: ConfigSanitizer):403        self.sanitizer = sanitizer404 405    # runtime_params is for parameters which is only configurable on runtime, such as tokenizer406    def generate(self, user_config: dict, argparse_namespace: argparse.Namespace, **runtime_params) -> Blueprint:407        sanitized_user_config = self.sanitizer.sanitize_user_config(user_config)408        sanitized_argparse_namespace = self.sanitizer.sanitize_argparse_namespace(argparse_namespace)409 410        # convert argparse namespace to dict like config411        # NOTE: it is ok to have extra entries in dict412        optname_map = self.sanitizer.ARGPARSE_OPTNAME_TO_CONFIG_OPTNAME413        argparse_config = {414            optname_map.get(optname, optname): value for optname, value in vars(sanitized_argparse_namespace).items()415        }416 417        general_config = sanitized_user_config.get("general", {})418 419        dataset_blueprints = []420        for dataset_config in sanitized_user_config.get("datasets", []):421            # NOTE: if subsets have no "metadata_file", these are DreamBooth datasets/subsets422            subsets = dataset_config.get("subsets", [])423            is_dreambooth = all(["metadata_file" not in subset for subset in subsets])424            is_controlnet = all(["conditioning_data_dir" in subset for subset in subsets])425            if is_controlnet:426                subset_params_klass = ControlNetSubsetParams427                dataset_params_klass = ControlNetDatasetParams428            elif is_dreambooth:429                subset_params_klass = DreamBoothSubsetParams430                dataset_params_klass = DreamBoothDatasetParams431            else:432                subset_params_klass = FineTuningSubsetParams433                dataset_params_klass = FineTuningDatasetParams434 435            subset_blueprints = []436            for subset_config in subsets:437                params = self.generate_params_by_fallbacks(438                    subset_params_klass, [subset_config, dataset_config, general_config, argparse_config, runtime_params]439                )440                subset_blueprints.append(SubsetBlueprint(params))441 442            params = self.generate_params_by_fallbacks(443                dataset_params_klass, [dataset_config, general_config, argparse_config, runtime_params]444            )445            dataset_blueprints.append(DatasetBlueprint(is_dreambooth, is_controlnet, params, subset_blueprints))446 447        dataset_group_blueprint = DatasetGroupBlueprint(dataset_blueprints)448 449        return Blueprint(dataset_group_blueprint)450 451    @staticmethod452    def generate_params_by_fallbacks(param_klass, fallbacks: Sequence[dict]):453        name_map = BlueprintGenerator.BLUEPRINT_PARAM_NAME_TO_CONFIG_OPTNAME454        search_value = BlueprintGenerator.search_value455        default_params = asdict(param_klass())456        param_names = default_params.keys()457 458        params = {name: search_value(name_map.get(name, name), fallbacks, default_params.get(name)) for name in param_names}459 460        return param_klass(**params)461 462    @staticmethod463    def search_value(key: str, fallbacks: Sequence[dict], default_value=None):464        for cand in fallbacks:465            value = cand.get(key)466            if value is not None:467                return value468 469        return default_value470 471 472def generate_dataset_group_by_blueprint(dataset_group_blueprint: DatasetGroupBlueprint):473    datasets: List[Union[DreamBoothDataset, FineTuningDataset, ControlNetDataset]] = []474 475    for dataset_blueprint in dataset_group_blueprint.datasets:476        if dataset_blueprint.is_controlnet:477            subset_klass = ControlNetSubset478            dataset_klass = ControlNetDataset479        elif dataset_blueprint.is_dreambooth:480            subset_klass = DreamBoothSubset481            dataset_klass = DreamBoothDataset482        else:483            subset_klass = FineTuningSubset484            dataset_klass = FineTuningDataset485 486        subsets = [subset_klass(**asdict(subset_blueprint.params)) for subset_blueprint in dataset_blueprint.subsets]487        dataset = dataset_klass(subsets=subsets, **asdict(dataset_blueprint.params))488        datasets.append(dataset)489 490    # print info491    info = ""492    for i, dataset in enumerate(datasets):493        is_dreambooth = isinstance(dataset, DreamBoothDataset)494        is_controlnet = isinstance(dataset, ControlNetDataset)495        info += dedent(496            f"""\497      [Dataset {i}]498        batch_size: {dataset.batch_size}499        resolution: {(dataset.width, dataset.height)}500        enable_bucket: {dataset.enable_bucket}501        network_multiplier: {dataset.network_multiplier}502    """503        )504 505        if dataset.enable_bucket:506            info += indent(507                dedent(508                    f"""\509        min_bucket_reso: {dataset.min_bucket_reso}510        max_bucket_reso: {dataset.max_bucket_reso}511        bucket_reso_steps: {dataset.bucket_reso_steps}512        bucket_no_upscale: {dataset.bucket_no_upscale}513      \n"""514                ),515                "  ",516            )517        else:518            info += "\n"519 520        for j, subset in enumerate(dataset.subsets):521            info += indent(522                dedent(523                    f"""\524        [Subset {j} of Dataset {i}]525          image_dir: "{subset.image_dir}"526          image_count: {subset.img_count}527          num_repeats: {subset.num_repeats}528          shuffle_caption: {subset.shuffle_caption}529          keep_tokens: {subset.keep_tokens}530          keep_tokens_separator: {subset.keep_tokens_separator}531          caption_separator: {subset.caption_separator}532          secondary_separator: {subset.secondary_separator}533          enable_wildcard: {subset.enable_wildcard}534          caption_dropout_rate: {subset.caption_dropout_rate}535          caption_dropout_every_n_epoches: {subset.caption_dropout_every_n_epochs}536          caption_tag_dropout_rate: {subset.caption_tag_dropout_rate}537          caption_prefix: {subset.caption_prefix}538          caption_suffix: {subset.caption_suffix}539          color_aug: {subset.color_aug}540          flip_aug: {subset.flip_aug}541          face_crop_aug_range: {subset.face_crop_aug_range}542          random_crop: {subset.random_crop}543          token_warmup_min: {subset.token_warmup_min},544          token_warmup_step: {subset.token_warmup_step},545          alpha_mask: {subset.alpha_mask},546      """547                ),548                "  ",549            )550 551            if is_dreambooth:552                info += indent(553                    dedent(554                        f"""\555          is_reg: {subset.is_reg}556          class_tokens: {subset.class_tokens}557          caption_extension: {subset.caption_extension}558        \n"""559                    ),560                    "    ",561                )562            elif not is_controlnet:563                info += indent(564                    dedent(565                        f"""\566          metadata_file: {subset.metadata_file}567        \n"""568                    ),569                    "    ",570                )571 572    logger.info(f"{info}")573 574    # make buckets first because it determines the length of dataset575    # and set the same seed for all datasets576    seed = random.randint(0, 2**31)  # actual seed is seed + epoch_no577    for i, dataset in enumerate(datasets):578        logger.info(f"[Dataset {i}]")579        dataset.make_buckets()580        dataset.set_seed(seed)581 582    return DatasetGroup(datasets)583 584 585def generate_dreambooth_subsets_config_by_subdirs(train_data_dir: Optional[str] = None, reg_data_dir: Optional[str] = None):586    def extract_dreambooth_params(name: str) -> Tuple[int, str]:587        tokens = name.split("_")588        try:589            n_repeats = int(tokens[0])590        except ValueError as e:591            logger.warning(f"ignore directory without repeats / 繰り返し回数のないディレクトリを無視します: {name}")592            return 0, ""593        caption_by_folder = "_".join(tokens[1:])594        return n_repeats, caption_by_folder595 596    def generate(base_dir: Optional[str], is_reg: bool):597        if base_dir is None:598            return []599 600        base_dir: Path = Path(base_dir)601        if not base_dir.is_dir():602            return []603 604        subsets_config = []605        for subdir in base_dir.iterdir():606            if not subdir.is_dir():607                continue608 609            num_repeats, class_tokens = extract_dreambooth_params(subdir.name)610            if num_repeats < 1:611                continue612 613            subset_config = {"image_dir": str(subdir), "num_repeats": num_repeats, "is_reg": is_reg, "class_tokens": class_tokens}614            subsets_config.append(subset_config)615 616        return subsets_config617 618    subsets_config = []619    subsets_config += generate(train_data_dir, False)620    subsets_config += generate(reg_data_dir, True)621 622    return subsets_config623 624 625def generate_controlnet_subsets_config_by_subdirs(626    train_data_dir: Optional[str] = None, conditioning_data_dir: Optional[str] = None, caption_extension: str = ".txt"627):628    def generate(base_dir: Optional[str]):629        if base_dir is None:630            return []631 632        base_dir: Path = Path(base_dir)633        if not base_dir.is_dir():634            return []635 636        subsets_config = []637        subset_config = {638            "image_dir": train_data_dir,639            "conditioning_data_dir": conditioning_data_dir,640            "caption_extension": caption_extension,641            "num_repeats": 1,642        }643        subsets_config.append(subset_config)644 645        return subsets_config646 647    subsets_config = []648    subsets_config += generate(train_data_dir)649 650    return subsets_config651 652 653def load_user_config(file: str) -> dict:654    file: Path = Path(file)655    if not file.is_file():656        raise ValueError(f"file not found / ファイルが見つかりません: {file}")657 658    if file.name.lower().endswith(".json"):659        try:660            with open(file, "r") as f:661                config = json.load(f)662        except Exception:663            logger.error(664                f"Error on parsing JSON config file. Please check the format. / JSON 形式の設定ファイルの読み込みに失敗しました。文法が正しいか確認してください。: {file}"665            )666            raise667    elif file.name.lower().endswith(".toml"):668        try:669            config = toml.load(file)670        except Exception:671            logger.error(672                f"Error on parsing TOML config file. Please check the format. / TOML 形式の設定ファイルの読み込みに失敗しました。文法が正しいか確認してください。: {file}"673            )674            raise675    else:676        raise ValueError(f"not supported config file format / 対応していない設定ファイルの形式です: {file}")677 678    return config679 680 681# for config test682if __name__ == "__main__":683    parser = argparse.ArgumentParser()684    parser.add_argument("--support_dreambooth", action="store_true")685    parser.add_argument("--support_finetuning", action="store_true")686    parser.add_argument("--support_controlnet", action="store_true")687    parser.add_argument("--support_dropout", action="store_true")688    parser.add_argument("dataset_config")689    config_args, remain = parser.parse_known_args()690 691    parser = argparse.ArgumentParser()692    train_util.add_dataset_arguments(693        parser, config_args.support_dreambooth, config_args.support_finetuning, config_args.support_dropout694    )695    train_util.add_training_arguments(parser, config_args.support_dreambooth)696    argparse_namespace = parser.parse_args(remain)697    train_util.prepare_dataset_args(argparse_namespace, config_args.support_finetuning)698 699    logger.info("[argparse_namespace]")700    logger.info(f"{vars(argparse_namespace)}")701 702    user_config = load_user_config(config_args.dataset_config)703 704    logger.info("")705    logger.info("[user_config]")706    logger.info(f"{user_config}")707 708    sanitizer = ConfigSanitizer(709        config_args.support_dreambooth, config_args.support_finetuning, config_args.support_controlnet, config_args.support_dropout710    )711    sanitized_user_config = sanitizer.sanitize_user_config(user_config)712 713    logger.info("")714    logger.info("[sanitized_user_config]")715    logger.info(f"{sanitized_user_config}")716 717    blueprint = BlueprintGenerator(sanitizer).generate(user_config, argparse_namespace)718 719    logger.info("")720    logger.info("[blueprint]")721    logger.info(f"{blueprint}")722