InvokeAI/ldm/invoke/config/model_install.py

505 lines
18 KiB
Python
Raw Normal View History

2023-02-13 04:59:18 +00:00
#!/usr/bin/env python
# Copyright (c) 2022 Lincoln D. Stein (https://github.com/lstein)
# Before running stable-diffusion on an internet-isolated machine,
# run this script from one with internet connectivity. The
# two machines must share a common .cache directory.
2023-02-21 19:12:57 +00:00
"""
This is the npyscreen frontend to the model installation application.
2023-02-17 19:34:48 +00:00
The work is actually done in backend code in model_install_backend.py.
2023-02-21 19:12:57 +00:00
"""
2023-02-13 04:59:18 +00:00
import argparse
import os
import sys
from argparse import Namespace
2023-02-21 19:12:57 +00:00
from pathlib import Path
from typing import List
2023-02-13 04:59:18 +00:00
import npyscreen
import torch
from npyscreen import widget
2023-02-13 04:59:18 +00:00
from omegaconf import OmegaConf
from shutil import get_terminal_size
2023-02-13 04:59:18 +00:00
from invokeai.backend.devices import choose_precision, choose_torch_device
from ..globals import Globals, global_config_dir
from .model_install_backend import (Dataset_path, default_config_file,
2023-02-21 19:12:57 +00:00
default_dataset, get_root,
install_requested_models,
recommended_datasets,
)
2023-02-22 02:33:44 +00:00
from .widgets import (MultiSelectColumns, TextBox,
OffsetButtonPress, CenteredTitleText,
set_min_terminal_size,
)
# minimum size for the UI
MIN_COLS = 120
MIN_LINES = 45
2023-02-13 04:59:18 +00:00
class addModelsForm(npyscreen.FormMultiPage):
# for responsive resizing - disabled
#FIX_MINIMUM_SIZE_WHEN_CREATED = False
2023-02-22 02:33:44 +00:00
2023-02-19 21:55:09 +00:00
def __init__(self, parentApp, name, multipage=False, *args, **keywords):
2023-02-21 19:12:57 +00:00
self.multipage = multipage
2023-02-13 04:59:18 +00:00
self.initial_models = OmegaConf.load(Dataset_path)
try:
self.existing_models = OmegaConf.load(default_config_file())
2023-02-13 04:59:18 +00:00
except:
self.existing_models = dict()
self.starter_model_list = [
x for x in list(self.initial_models.keys()) if x not in self.existing_models
]
2023-02-21 19:12:57 +00:00
self.installed_models = dict()
2023-02-19 21:55:09 +00:00
super().__init__(parentApp=parentApp, name=name, *args, **keywords)
2023-02-13 04:59:18 +00:00
def create(self):
window_width, window_height = get_terminal_size()
starter_model_labels = self._get_starter_model_labels()
2023-02-13 04:59:18 +00:00
recommended_models = [
x
for x in self.starter_model_list
if self.initial_models[x].get("recommended", False)
]
self.installed_models = sorted(
2023-02-21 19:12:57 +00:00
[x for x in list(self.initial_models.keys()) if x in self.existing_models]
)
self.nextrely -= 1
self.add_widget_intelligent(
npyscreen.FixedText,
2023-02-21 19:12:57 +00:00
value="Use ctrl-N and ctrl-P to move to the <N>ext and <P>revious fields,",
editable=False,
2023-02-22 02:33:44 +00:00
color='CAUTION',
)
self.add_widget_intelligent(
npyscreen.FixedText,
2023-02-22 02:33:44 +00:00
value="Use cursor arrows to make a selection, and space to toggle checkboxes.",
editable=False,
2023-02-22 02:33:44 +00:00
color='CAUTION'
)
self.nextrely += 1
if len(self.installed_models) > 0:
self.add_widget_intelligent(
2023-02-22 02:33:44 +00:00
CenteredTitleText,
name="== INSTALLED STARTER MODELS ==",
editable=False,
color="CONTROL",
)
2023-02-22 02:33:44 +00:00
self.nextrely -= 1
self.add_widget_intelligent(
CenteredTitleText,
name="Currently installed starter models. Uncheck to delete:",
editable=False,
labelColor="CAUTION",
)
self.nextrely -= 1
columns = self._get_columns()
self.previously_installed_models = self.add_widget_intelligent(
MultiSelectColumns,
columns=columns,
values=self.installed_models,
2023-02-21 19:12:57 +00:00
value=[x for x in range(0, len(self.installed_models))],
max_height=1 + len(self.installed_models) // columns,
relx=4,
slow_scroll=True,
2023-02-21 19:12:57 +00:00
scroll_exit=True,
)
self.purge_deleted = self.add_widget_intelligent(
npyscreen.Checkbox,
2023-02-21 19:12:57 +00:00
name="Purge deleted models from disk",
value=False,
2023-02-21 19:12:57 +00:00
scroll_exit=True,
2023-02-22 02:33:44 +00:00
relx=4,
)
self.nextrely += 1
if len(self.starter_model_list) > 0:
self.add_widget_intelligent(
CenteredTitleText,
name="== STARTER MODELS (recommended ones selected) ==",
editable=False,
color="CONTROL",
)
self.nextrely -= 1
self.add_widget_intelligent(
CenteredTitleText,
name="Select from a starter set of Stable Diffusion models from HuggingFace.",
editable=False,
labelColor="CAUTION",
)
self.nextrely -= 1
# if user has already installed some initial models, then don't patronize them
# by showing more recommendations
show_recommended = not self.existing_models
self.models_selected = self.add_widget_intelligent(
npyscreen.MultiSelect,
name="Install Starter Models",
values=starter_model_labels,
value=[
self.starter_model_list.index(x)
for x in self.starter_model_list
if show_recommended and x in recommended_models
],
max_height=len(starter_model_labels) + 1,
relx=4,
scroll_exit=True,
)
2023-02-22 02:33:44 +00:00
self.add_widget_intelligent(
CenteredTitleText,
name='== IMPORT LOCAL AND REMOTE MODELS ==',
editable=False,
color="CONTROL",
)
self.nextrely -= 1
for line in [
2023-02-22 02:33:44 +00:00
"In the box below, enter URLs, file paths, or HuggingFace repository IDs.",
"Separate model names by lines or whitespace (Use shift-control-V to paste):",
]:
self.add_widget_intelligent(
2023-02-22 02:33:44 +00:00
CenteredTitleText,
name=line,
editable=False,
2023-02-22 02:33:44 +00:00
labelColor="CONTROL",
relx = 4,
)
self.nextrely -= 1
self.import_model_paths = self.add_widget_intelligent(
TextBox,
max_height=7,
scroll_exit=True,
editable=True,
relx=4
)
self.nextrely += 1
2023-02-21 19:12:57 +00:00
self.show_directory_fields = self.add_widget_intelligent(
npyscreen.FormControlCheckbox,
2023-02-21 19:12:57 +00:00
name="Select a directory for models to import",
value=False,
)
self.autoload_directory = self.add_widget_intelligent(
npyscreen.TitleFilename,
2023-02-21 19:12:57 +00:00
name="Directory (<tab> autocompletes):",
select_dir=True,
must_exist=True,
use_two_lines=False,
2023-02-21 19:12:57 +00:00
labelColor="DANGER",
begin_entry_at=34,
scroll_exit=True,
)
self.autoscan_on_startup = self.add_widget_intelligent(
npyscreen.Checkbox,
2023-02-21 19:12:57 +00:00
name="Scan this directory each time InvokeAI starts for new models to import",
value=False,
2023-02-21 19:12:57 +00:00
relx=4,
scroll_exit=True,
)
self.nextrely += 1
self.convert_models = self.add_widget_intelligent(
npyscreen.TitleSelectOne,
2023-02-21 19:12:57 +00:00
name="== CONVERT IMPORTED MODELS INTO DIFFUSERS==",
values=["Keep original format", "Convert to diffusers"],
value=0,
begin_entry_at=4,
max_height=4,
hidden=True, # will appear when imported models box is edited
scroll_exit=True,
)
self.cancel = self.add_widget_intelligent(
npyscreen.ButtonPress,
2023-02-21 19:12:57 +00:00
name="CANCEL",
rely=-3,
when_pressed_function=self.on_cancel,
)
2023-02-21 19:12:57 +00:00
done_label = "DONE"
back_label = "BACK"
button_length = len(done_label)
button_offset = 0
if self.multipage:
2023-02-21 19:12:57 +00:00
button_length += len(back_label) + 1
button_offset += len(back_label) + 1
self.back_button = self.add_widget_intelligent(
2023-02-22 02:33:44 +00:00
OffsetButtonPress,
name=back_label,
2023-02-21 19:12:57 +00:00
relx=(window_width - button_length) // 2,
2023-02-22 02:33:44 +00:00
offset=-3,
2023-02-21 19:12:57 +00:00
rely=-3,
when_pressed_function=self.on_back,
)
self.ok_button = self.add_widget_intelligent(
2023-02-22 02:33:44 +00:00
OffsetButtonPress,
name=done_label,
2023-02-22 02:33:44 +00:00
offset=+3,
2023-02-21 19:12:57 +00:00
relx=button_offset + 1 + (window_width - button_length) // 2,
rely=-3,
when_pressed_function=self.on_ok,
)
2023-02-21 19:12:57 +00:00
for i in [self.autoload_directory, self.autoscan_on_startup]:
self.show_directory_fields.addVisibleWhenSelected(i)
self.show_directory_fields.when_value_edited = self._clear_scan_directory
self.import_model_paths.when_value_edited = self._show_hide_convert
self.autoload_directory.when_value_edited = self._show_hide_convert
def resize(self):
super().resize()
if hasattr(self,'models_selected'):
self.models_selected.values = self._get_starter_model_labels()
def _clear_scan_directory(self):
if not self.show_directory_fields.value:
2023-02-21 19:12:57 +00:00
self.autoload_directory.value = ""
def _show_hide_convert(self):
2023-02-21 19:12:57 +00:00
model_paths = self.import_model_paths.value or ""
autoload_directory = self.autoload_directory.value or ""
self.convert_models.hidden = (
len(model_paths) == 0 and len(autoload_directory) == 0
)
def _get_starter_model_labels(self) -> List[str]:
window_width, window_height = get_terminal_size()
label_width = 25
checkbox_width = 4
spacing_width = 2
description_width = window_width - label_width - checkbox_width - spacing_width
im = self.initial_models
names = self.starter_model_list
2023-02-21 19:12:57 +00:00
descriptions = [
im[x].description[0 : description_width - 3] + "..."
if len(im[x].description) > description_width
else im[x].description
for x in names
]
return [
2023-02-21 19:12:57 +00:00
f"%-{label_width}s %s" % (names[x], descriptions[x])
for x in range(0, len(names))
]
2023-02-13 04:59:18 +00:00
2023-02-21 19:12:57 +00:00
def _get_columns(self) -> int:
window_width, window_height = get_terminal_size()
2023-02-21 19:12:57 +00:00
cols = (
4
if window_width > 240
else 3
if window_width > 160
else 2
if window_width > 80
else 1
)
return min(cols, len(self.installed_models))
2023-02-13 04:59:18 +00:00
def on_ok(self):
self.parentApp.setNextForm(None)
2023-02-13 04:59:18 +00:00
self.editing = False
self.parentApp.user_cancelled = False
self.marshall_arguments()
2023-02-13 04:59:18 +00:00
def on_back(self):
self.parentApp.switchFormPrevious()
self.editing = False
def on_cancel(self):
if npyscreen.notify_yes_no(
2023-02-21 19:12:57 +00:00
"Are you sure you want to cancel?\nYou may re-run this script later using the invoke.sh or invoke.bat command.\n"
):
self.parentApp.setNextForm(None)
self.parentApp.user_cancelled = True
self.editing = False
def marshall_arguments(self):
2023-02-21 19:12:57 +00:00
"""
Assemble arguments and store as attributes of the application:
.starter_models: dict of model names to install from INITIAL_CONFIGURE.yaml
True => Install
False => Remove
.scan_directory: Path to a directory of models to scan and import
.autoscan_on_startup: True if invokeai should scan and import at startup time
.import_model_paths: list of URLs, repo_ids and file paths to import
.convert_to_diffusers: if True, convert legacy checkpoints into diffusers
2023-02-21 19:12:57 +00:00
"""
# we're using a global here rather than storing the result in the parentapp
# due to some bug in npyscreen that is causing attributes to be lost
selections = self.parentApp.user_selections
# starter models to install/remove
if hasattr(self,'models_selected'):
starter_models = dict(
map(
lambda x: (self.starter_model_list[x], True), self.models_selected.value
)
2023-02-21 19:12:57 +00:00
)
else:
starter_models = dict()
2023-02-21 19:12:57 +00:00
selections.purge_deleted_models = False
if hasattr(self, "previously_installed_models"):
unchecked = [
self.previously_installed_models.values[x]
2023-02-21 19:12:57 +00:00
for x in range(0, len(self.previously_installed_models.values))
if x not in self.previously_installed_models.value
]
2023-02-21 19:12:57 +00:00
starter_models.update(map(lambda x: (x, False), unchecked))
selections.purge_deleted_models = self.purge_deleted.value
2023-02-21 19:12:57 +00:00
selections.starter_models = starter_models
# load directory and whether to scan on startup
if self.show_directory_fields.value:
selections.scan_directory = self.autoload_directory.value
selections.autoscan_on_startup = self.autoscan_on_startup.value
else:
selections.scan_directory = None
selections.autoscan_on_startup = False
# URLs and the like
selections.import_model_paths = self.import_model_paths.value.split()
selections.convert_to_diffusers = self.convert_models.value[0] == 1
2023-02-21 19:12:57 +00:00
class AddModelApplication(npyscreen.NPSAppManaged):
def __init__(self):
2023-02-13 04:59:18 +00:00
super().__init__()
self.user_cancelled = False
self.user_selections = Namespace(
2023-02-21 19:12:57 +00:00
starter_models=None,
purge_deleted_models=False,
scan_directory=None,
autoscan_on_startup=None,
import_model_paths=None,
convert_to_diffusers=None,
)
2023-02-13 04:59:18 +00:00
def onStart(self):
npyscreen.setTheme(npyscreen.Themes.DefaultTheme)
self.main_form = self.addForm(
2023-02-21 19:12:57 +00:00
"MAIN", addModelsForm, name="Install Stable Diffusion Models"
2023-02-13 04:59:18 +00:00
)
# --------------------------------------------------------
def process_and_execute(opt: Namespace, selections: Namespace):
2023-02-21 19:12:57 +00:00
models_to_remove = [
x for x in selections.starter_models if not selections.starter_models[x]
]
models_to_install = [
x for x in selections.starter_models if selections.starter_models[x]
]
directory_to_scan = selections.scan_directory
scan_at_startup = selections.autoscan_on_startup
potential_models_to_install = selections.import_model_paths
convert_to_diffusers = selections.convert_to_diffusers
install_requested_models(
2023-02-21 19:12:57 +00:00
install_initial_models=models_to_install,
remove_models=models_to_remove,
scan_directory=Path(directory_to_scan) if directory_to_scan else None,
external_models=potential_models_to_install,
scan_at_startup=scan_at_startup,
convert_to_diffusers=convert_to_diffusers,
precision="float32"
if opt.full_precision
else choose_precision(torch.device(choose_torch_device())),
purge_deleted=selections.purge_deleted_models,
config_file_path=Path(opt.config_file) if opt.config_file else None,
)
2023-02-21 19:12:57 +00:00
2023-02-13 04:59:18 +00:00
# --------------------------------------------------------
def select_and_download_models(opt: Namespace):
2023-02-21 19:12:57 +00:00
precision = (
"float32"
if opt.full_precision
else choose_precision(torch.device(choose_torch_device()))
)
2023-02-13 04:59:18 +00:00
if opt.default_only:
install_requested_models(
2023-02-21 19:12:57 +00:00
install_initial_models=default_dataset(),
precision=precision,
)
elif opt.yes_to_all:
install_requested_models(
2023-02-21 19:12:57 +00:00
install_initial_models=recommended_datasets(),
precision=precision,
)
2023-02-13 04:59:18 +00:00
else:
set_min_terminal_size(MIN_COLS, MIN_LINES)
installApp = AddModelApplication()
installApp.run()
if not installApp.user_cancelled:
process_and_execute(opt, installApp.user_selections)
2023-02-13 04:59:18 +00:00
2023-02-21 19:12:57 +00:00
2023-02-13 04:59:18 +00:00
# -------------------------------------
def main():
parser = argparse.ArgumentParser(description="InvokeAI model downloader")
parser.add_argument(
"--full-precision",
dest="full_precision",
action=argparse.BooleanOptionalAction,
type=bool,
default=False,
help="use 32-bit weights instead of faster 16-bit weights",
)
parser.add_argument(
"--yes",
"-y",
dest="yes_to_all",
action="store_true",
help='answer "yes" to all prompts',
)
parser.add_argument(
"--default_only",
action="store_true",
help="only install the default model",
)
parser.add_argument(
"--config_file",
"-c",
dest="config_file",
type=str,
default=None,
help="path to configuration file to create",
)
parser.add_argument(
"--root_dir",
dest="root",
type=str,
default=None,
help="path to root of install directory",
)
opt = parser.parse_args()
# setting a global here
Globals.root = os.path.expanduser(get_root(opt.root) or "")
if not global_config_dir().exists():
2023-02-21 19:12:57 +00:00
print(
">> Your InvokeAI root directory is not set up. Calling invokeai-configure."
)
import ldm.invoke.config.invokeai_configure
2023-02-21 19:12:57 +00:00
ldm.invoke.config.invokeai_configure.main()
sys.exit(0)
2023-02-13 04:59:18 +00:00
try:
select_and_download_models(opt)
except AssertionError as e:
print(str(e))
sys.exit(-1)
2023-02-13 04:59:18 +00:00
except KeyboardInterrupt:
print("\nGoodbye! Come back soon.")
except widget.NotEnoughSpaceForWidget as e:
if str(e).startswith("Height of 1 allocated"):
print(
"** Insufficient vertical space for the interface. Please make your window taller and try again"
)
2023-02-21 19:12:57 +00:00
elif str(e).startswith("addwstr"):
print(
2023-02-21 19:12:57 +00:00
"** Insufficient horizontal space for the interface. Please make your window wider and try again."
)
2023-02-21 19:12:57 +00:00
2023-02-13 04:59:18 +00:00
# -------------------------------------
if __name__ == "__main__":
main()