Allex21/LT
0
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 