From 5fd7d71a7a469e9490ad979f4dca2d2d140a8259 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Fri, 18 Nov 2022 21:14:28 +0000 Subject: [PATCH 1/2] remove several debugging messages - dangling debug messages in several files, introduced during testing of the external root directory - these need to be removed before they are interpreted as errors by users --- backend/invoke_ai_web_server.py | 1 - ldm/invoke/model_cache.py | 2 -- ldm/invoke/restoration/outcrop.py | 2 -- 3 files changed, 5 deletions(-) diff --git a/backend/invoke_ai_web_server.py b/backend/invoke_ai_web_server.py index efdc932eef..c123f4435c 100644 --- a/backend/invoke_ai_web_server.py +++ b/backend/invoke_ai_web_server.py @@ -65,7 +65,6 @@ class InvokeAIWebServer: if opt.cors: socketio_args["cors_allowed_origins"] = opt.cors - print(f'DEBUG: static_folder should be at {os.path.join(args.root_dir,"frontend/dist")}') self.app = Flask( __name__, static_url_path="", static_folder=os.path.join(args.root_dir,"frontend/dist") ) diff --git a/ldm/invoke/model_cache.py b/ldm/invoke/model_cache.py index 44360b36a9..fc1efd3df6 100644 --- a/ldm/invoke/model_cache.py +++ b/ldm/invoke/model_cache.py @@ -111,13 +111,11 @@ class ModelCache(object): Set the default model. The change will not take effect until you call model_cache.commit() ''' - print(f'DEBUG: before set_default_model()\n{OmegaConf.to_yaml(self.config)}') assert model_name in self.models,f"unknown model '{model_name}'" config = self.config for model in config: config[model].pop('default',None) config[model_name]['default'] = True - print(f'DEBUG: after set_default_model():\n{OmegaConf.to_yaml(self.config)}') def list_models(self) -> dict: ''' diff --git a/ldm/invoke/restoration/outcrop.py b/ldm/invoke/restoration/outcrop.py index 1d3c985910..2587bb5776 100644 --- a/ldm/invoke/restoration/outcrop.py +++ b/ldm/invoke/restoration/outcrop.py @@ -31,8 +31,6 @@ class Outcrop(object): preferred_seed = orig_opt.seed if orig_opt.seed >= 0 else seed image_callback(img,preferred_seed,use_prefix=prefix,**kwargs) - print(f'DEBUG: seed={opt.seed or orig_opt.seed}') - print(f'DEBUG: prompt={opt.prompt}') result= self.generate.prompt2image( opt.prompt, seed = opt.seed or orig_opt.seed, From 0381a853b532ecc0eac7eac1702600a8e5905eda Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Sat, 19 Nov 2022 19:20:28 +0000 Subject: [PATCH 2/2] add interactive configuration to the model loader - Loader is renamed `configure_invokeai.py`, but `preload_models.py` is retained (as a shell) for backward compatibility - At startup, if no runtime root directory exists and no `.invokeai` startup file is present, user will be prompted to select the runtime and outputs directories. - Also expanded the number of initial models offered to the user to include the most "liked" ones from HuggingFace, including the two trinart models, the PaperCut model, and the VoxelArt model. - Created a configuration file for initial models to be offered to the user, at configs/INITIAL_MODELS.yaml --- configs/INITIAL_MODELS.yaml | 80 ++++++++++ installer/install.bat | 2 +- installer/install.sh | 2 +- ldm/invoke/readline.py | 21 ++- .../{load_models.py => configure_invokeai.py} | 143 +++++++++--------- scripts/preload_models.py | 4 +- 6 files changed, 179 insertions(+), 73 deletions(-) create mode 100644 configs/INITIAL_MODELS.yaml rename scripts/{load_models.py => configure_invokeai.py} (87%) diff --git a/configs/INITIAL_MODELS.yaml b/configs/INITIAL_MODELS.yaml new file mode 100644 index 0000000000..52a3efef39 --- /dev/null +++ b/configs/INITIAL_MODELS.yaml @@ -0,0 +1,80 @@ +stable-diffusion-1.5: + description: The newest Stable Diffusion version 1.5 weight file (4.27 GB) + repo_id: runwayml/stable-diffusion-v1-5 + config: v1-inference.yaml + file: v1-5-pruned-emaonly.ckpt + recommended: true + width: 512 + height: 512 +inpainting-1.5: + description: RunwayML SD 1.5 model optimized for inpainting (4.27 GB) + repo_id: runwayml/stable-diffusion-inpainting + config: v1-inpainting-inference.yaml + file: sd-v1-5-inpainting.ckpt + recommended: True + width: 512 + height: 512 +ft-mse-improved-autoencoder-840000: + description: StabilityAI improved autoencoder fine-tuned for human faces (recommended; 335 MB) + repo_id: stabilityai/sd-vae-ft-mse-original + config: VAE/default + file: vae-ft-mse-840000-ema-pruned.ckpt + recommended: True + width: 512 + height: 512 +stable-diffusion-1.4: + description: The original Stable Diffusion version 1.4 weight file (4.27 GB) + repo_id: CompVis/stable-diffusion-v-1-4-original + config: v1-inference.yaml + file: sd-v1-4.ckpt + recommended: False + width: 512 + height: 512 +waifu-diffusion-1.3: + description: Stable Diffusion 1.4 fine tuned on anime-styled images (4.27) + repo_id: hakurei/waifu-diffusion-v1-3 + config: v1-inference.yaml + file: model-epoch09-float32.ckpt + recommended: False + width: 512 + height: 512 +trinart-2.0: + description: An SD model finetuned with ~40,000 assorted high resolution manga/anime-style pictures (2.13 GB) + repo_id: naclbit/trinart_stable_diffusion_v2 + config: v1-inference.yaml + file: trinart2_step95000.ckpt + recommended: False + width: 512 + height: 512 +trinart_characters-1.0: + description: An SD model finetuned with 19.2M anime/manga style images (2.13 GB) + repo_id: naclbit/trinart_characters_19.2m_stable_diffusion_v1 + config: v1-inference.yaml + file: trinart_characters_it4_v1.ckpt + recommended: False + width: 512 + height: 512 +trinart_vae: + description: Custom autoencoder for trinart_characters + repo_id: naclbit/trinart_characters_19.2m_stable_diffusion_v1 + config: VAE/trinart + file: autoencoder_fix_kl-f8-trinart_characters.ckpt + recommended: False + width: 512 + height: 512 +papercut-1.0: + description: SD 1.5 fine-tuned for papercut art (use "PaperCut" in your prompts) (2.13 GB) + repo_id: Fictiverse/Stable_Diffusion_PaperCut_Model + config: v1-inference.yaml + file: PaperCut_v1.ckpt + recommended: False + width: 512 + height: 512 +voxel_art-1.0: + description: Stable Diffusion trained on voxel art (use "VoxelArt" in your prompts) (4.27 GB) + repo_id: Fictiverse/Stable_Diffusion_VoxelArt_Model + config: v1-inference.yaml + file: VoxelArt_v1.ckpt + recommended: False + width: 512 + height: 512 diff --git a/installer/install.bat b/installer/install.bat index 131b14e06e..426c788fea 100644 --- a/installer/install.bat +++ b/installer/install.bat @@ -133,7 +133,7 @@ if %errorlevel% neq 0 goto err_exit echo ***** Installed Python dependencies ***** @rem preload the models -call .venv\Scripts\python scripts\preload_models.py +call .venv\Scripts\python scripts\configure_invokeai.py set err_msg=----- model download clone failed ----- if %errorlevel% neq 0 goto err_exit diff --git a/installer/install.sh b/installer/install.sh index 68784f101a..406bb78c76 100755 --- a/installer/install.sh +++ b/installer/install.sh @@ -200,7 +200,7 @@ _err_exit $? _err_msg echo -e "\n***** Installed Python dependencies *****\n" # preload the models -.venv/bin/python3 scripts/preload_models.py +.venv/bin/python3 scripts/configure_invokeai.py _err_msg="\n----- model download clone failed -----\n" _err_exit $? _err_msg diff --git a/ldm/invoke/readline.py b/ldm/invoke/readline.py index ff468c9e7c..e0b0c7689b 100644 --- a/ldm/invoke/readline.py +++ b/ldm/invoke/readline.py @@ -188,6 +188,9 @@ class Completer(object): def set_default_dir(self, path): self.default_dir=path + def set_options(self,options): + self.options = options + def get_line(self,index): try: line = self.get_history_item(index) @@ -283,6 +286,7 @@ class Completer(object): partial_path = text else: switch,partial_path = match.groups() + partial_path = partial_path.lstrip() matches = list() @@ -308,7 +312,7 @@ class Completer(object): if not (node.endswith(extensions) or os.path.isdir(full_path)): continue - if not full_path.startswith(path): + if path and not full_path.startswith(path): continue if switch is None: @@ -348,6 +352,21 @@ class DummyCompleter(Completer): def set_line(self,line): print(f'# {line}') +def generic_completer(commands:list)->Completer: + if readline_available: + completer = Completer(commands,[]) + readline.set_completer(completer.complete) + readline.set_pre_input_hook(completer._pre_input_hook) + readline.set_completer_delims(' ') + readline.parse_and_bind('tab: complete') + readline.parse_and_bind('set print-completions-horizontally off') + readline.parse_and_bind('set page-completions on') + readline.parse_and_bind('set skip-completed-text on') + readline.parse_and_bind('set show-all-if-ambiguous on') + else: + completer = DummyCompleter(commands) + return completer + def get_completer(opt:Args, models=[])->Completer: if readline_available: completer = Completer(COMMANDS,models) diff --git a/scripts/load_models.py b/scripts/configure_invokeai.py similarity index 87% rename from scripts/load_models.py rename to scripts/configure_invokeai.py index 062f081506..46f652b15d 100755 --- a/scripts/load_models.py +++ b/scripts/configure_invokeai.py @@ -21,6 +21,7 @@ from pathlib import Path from getpass_asterisk import getpass_asterisk from transformers import CLIPTokenizer, CLIPTextModel from ldm.invoke.globals import Globals +from ldm.invoke.readline import generic_completer import traceback import requests @@ -34,55 +35,13 @@ transformers.logging.set_verbosity_error() #--------------------------globals----------------------- Model_dir = 'models' Weights_dir = 'ldm/stable-diffusion-v1/' +Dataset_path = './configs/INITIAL_MODELS.yaml' Default_config_file = './configs/models.yaml' SD_Configs = './configs/stable-diffusion' -Datasets = { - 'stable-diffusion-1.5': { - 'description': 'The newest Stable Diffusion version 1.5 weight file (4.27 GB)', - 'repo_id': 'runwayml/stable-diffusion-v1-5', - 'config': 'v1-inference.yaml', - 'file': 'v1-5-pruned-emaonly.ckpt', - 'recommended': True, - 'width': 512, - 'height': 512, - }, - 'inpainting-1.5': { - 'description': 'RunwayML SD 1.5 model optimized for inpainting (4.27 GB)', - 'repo_id': 'runwayml/stable-diffusion-inpainting', - 'config': 'v1-inpainting-inference.yaml', - 'file': 'sd-v1-5-inpainting.ckpt', - 'recommended': True, - 'width': 512, - 'height': 512, - }, - 'stable-diffusion-1.4': { - 'description': 'The original Stable Diffusion version 1.4 weight file (4.27 GB)', - 'repo_id': 'CompVis/stable-diffusion-v-1-4-original', - 'config': 'v1-inference.yaml', - 'file': 'sd-v1-4.ckpt', - 'recommended': False, - 'width': 512, - 'height': 512, - }, - 'waifu-diffusion-1.3': { - 'description': 'Stable Diffusion 1.4 fine tuned on anime-styled images (4.27)', - 'repo_id': 'hakurei/waifu-diffusion-v1-3', - 'config': 'v1-inference.yaml', - 'file': 'model-epoch09-float32.ckpt', - 'recommended': False, - 'width': 512, - 'height': 512, - }, - 'ft-mse-improved-autoencoder-840000': { - 'description': 'StabilityAI improved autoencoder fine-tuned for human faces (recommended; 335 MB)', - 'repo_id': 'stabilityai/sd-vae-ft-mse-original', - 'config': 'VAE', - 'file': 'vae-ft-mse-840000-ema-pruned.ckpt', - 'recommended': True, - 'width': 512, - 'height': 512, - }, -} + +Datasets = OmegaConf.load(Dataset_path) +completer = generic_completer(['yes','no']) + Config_preamble = '''# This file describes the alternative machine learning models # available to InvokeAI script. # @@ -120,6 +79,8 @@ Have fun! #--------------------------------------------- def yes_or_no(prompt:str, default_yes=True): + completer.set_options(['yes','no']) + completer.complete_extensions(None) # turn off path-completion mode default = "y" if default_yes else 'n' response = input(f'{prompt} [{default}] ') or default if default_yes: @@ -141,6 +102,8 @@ You may download the recommended models (about 10GB total), select a customized completely skip this step. ''' ) + completer.set_options(['recommended','customized','skip']) + completer.complete_extensions(None) # turn off path-completion mode selection = None while selection is None: choice = input('Download ecommended models, ustomize the list, or kip this step? [r]: ') @@ -399,15 +362,18 @@ def new_config_file_contents(successfully_downloaded:dict, config_file:str)->str conf = OmegaConf.create() # find the VAE file, if there is one - vae = None + vaes = {} default_selected = False for model in successfully_downloaded: - if Datasets[model]['config'] == 'VAE': - vae = Datasets[model]['file'] + a = Datasets[model]['config'].split('/') + if a[0] != 'VAE': + continue + vae_target = a[1] if len(a)>1 else 'default' + vaes[vae_target] = Datasets[model]['file'] for model in successfully_downloaded: - if Datasets[model]['config'] == 'VAE': # skip VAE entries + if Datasets[model]['config'].startswith('VAE'): # skip VAE entries continue stanza = conf[model] if model in conf else { } @@ -417,8 +383,12 @@ def new_config_file_contents(successfully_downloaded:dict, config_file:str)->str stanza['width'] = Datasets[model]['width'] stanza['height'] = Datasets[model]['height'] stanza.pop('default',None) # this will be set later - if vae: - stanza['vae'] = os.path.normpath(os.path.join(Model_dir,Weights_dir,vae)) + if vaes: + for target in vaes: + if re.search(target, model, flags=re.IGNORECASE): + stanza['vae'] = os.path.normpath(os.path.join(Model_dir,Weights_dir,vaes[target])) + else: + stanza['vae'] = os.path.normpath(os.path.join(Model_dir,Weights_dir,vaes['default'])) # BUG - the first stanza is always the default. User should select. if not default_selected: stanza['default'] = True @@ -575,7 +545,7 @@ def get_root(root:str=None)->str: else: init_file = os.path.expanduser(Globals.initfile) if not os.path.exists(init_file): - return '.' + return None # if we get here, then we read from initfile root = None @@ -587,31 +557,63 @@ def get_root(root:str=None)->str: match = re.search('--root\s*=?\s*"?([^"]+)"?',l) if match: root = match.groups()[0] - root = root or '.' - return root.strip() + root = root.strip() + return root #------------------------------------- -def initialize_rootdir(root:str): +def select_root(yes_to_all:bool=False): + default = os.path.expanduser('~/invokeai') + if (yes_to_all): + return default + completer.set_default_dir(default) + completer.complete_extensions(()) + completer.set_line(default) + return input(f"Select a directory in which to install InvokeAI's models and configuration files [{default}]: ") + +#------------------------------------- +def select_outputs(root:str,yes_to_all:bool=False): + default = os.path.normpath(os.path.join(root,'outputs')) + if (yes_to_all): + return default + completer.set_default_dir(os.path.expanduser('~')) + completer.complete_extensions(()) + completer.set_line(default) + return input('Select the default directory for image outputs [{default}]: ') + +#------------------------------------- +def initialize_rootdir(root:str,yes_to_all:bool=False): assert os.path.exists('./configs'),'Run this script from within the top level of the InvokeAI source code directory, "InvokeAI"' - print(f'** INITIALIZING INVOKEAI ROOT DIRECTORY **') - print(f'Creating a directory named {root} to contain InvokeAI models, configuration files and outputs.') - print(f'If you move this directory, please change its location using the --root option in "{Globals.initfile},') - print(f'or set the environment variable INVOKEAI_ROOT to the new location.\n') - for name in ('models','configs','outputs','scripts','frontend/dist'): + + print(f'** INITIALIZING INVOKEAI RUNTIME DIRECTORY **') + root = root or select_root(yes_to_all) + outputs = select_outputs(root,yes_to_all) + Globals.root = root + + print(f'InvokeAI models and configuration files will be placed into {root} and image outputs will be placed into {outputs}.') + print(f'\nYou may change these values at any time by editing the --root and --output_dir options in "{Globals.initfile}",') + print(f'You may also change the runtime directory by setting the environment variable INVOKEAI_ROOT.\n') + for name in ('models','configs','scripts','frontend/dist'): os.makedirs(os.path.join(root,name), exist_ok=True) for src in ('configs','scripts','frontend/dist'): dest = os.path.join(root,src) if not os.path.samefile(src,dest): shutil.copytree(src,dest,dirs_exist_ok=True) + os.makedirs(outputs) init_file = os.path.expanduser(Globals.initfile) if not os.path.exists(init_file): - print(f'Creating a basic initialization file at "{init_file}".\n') + print(f'Creating the initialization file at "{init_file}".\n') with open(init_file,'w') as f: f.write(f'''# InvokeAI initialization file +# This is the InvokeAI initialization file, which contains command-line default values. +# Feel free to edit. If anything goes wrong, you can re-initialize this file by deleting +# or renaming it and then running configure_invokeai.py again. + # The --root option below points to the folder in which InvokeAI stores its models, configs and outputs. -# Don't change it unless you know what you are doing! ---root="{Globals.root}" +--root="{root}" + +# the --outdir option controls the default location of image files. +--outdir="{outputs}" # You may place other frequently-used startup commands here, one or more per line. # Examples: @@ -622,6 +624,7 @@ def initialize_rootdir(root:str): ''' ) + #------------------------------------- class ProgressBar(): def __init__(self,model_name='file'): @@ -663,8 +666,9 @@ def main(): help='path to root of install directory') opt = parser.parse_args() + # setting a global here - Globals.root = os.path.expanduser(get_root(opt.root)) + Globals.root = os.path.expanduser(get_root(opt.root) or '') try: introduction() @@ -672,9 +676,12 @@ def main(): # We check for two files to see if the runtime directory is correctly initialized. # 1. a key stable diffusion config file # 2. the web front end static files - if not os.path.exists(os.path.join(Globals.root,'configs/stable-diffusion/v1-inference.yaml')) \ + if Globals.root == '' \ + or not os.path.exists(os.path.join(Globals.root,'configs/stable-diffusion/v1-inference.yaml')) \ or not os.path.exists(os.path.join(Globals.root,'frontend/dist')): - initialize_rootdir(Globals.root) + initialize_rootdir(Globals.root,opt.yes_to_all) + + print(f'(Initializing with runtime root {Globals.root})\n') if opt.interactive: print('** DOWNLOADING DIFFUSION WEIGHTS **') diff --git a/scripts/preload_models.py b/scripts/preload_models.py index 26b2a2e64f..e64d0b821d 100755 --- a/scripts/preload_models.py +++ b/scripts/preload_models.py @@ -5,8 +5,8 @@ # two machines must share a common .cache directory. import warnings -import load_models +import configure_invokeai if __name__ == '__main__': - load_models.main() + configure_invokeai.main()