From 3083356cf0f9200579802ff3a7d205f730a611dc Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Wed, 22 Feb 2023 19:18:07 -0500 Subject: [PATCH] installer enhancements - Detect when install failed for some reason and print helpful error message rather than stack trace. - Detect window size and resize to minimum acceptable values to provide better display of configure and install forms. --- installer/lib/installer.py | 16 ++++++++++++- ldm/invoke/config/invokeai_configure.py | 17 ++++++++------ ldm/invoke/config/model_install.py | 21 +++++++++++------ ldm/invoke/config/model_install_backend.py | 5 +++- ldm/invoke/config/widgets.py | 27 +++++++++++++++++++++- 5 files changed, 69 insertions(+), 17 deletions(-) diff --git a/installer/lib/installer.py b/installer/lib/installer.py index fb1fd2b217..a279c60e0c 100644 --- a/installer/lib/installer.py +++ b/installer/lib/installer.py @@ -346,7 +346,21 @@ class InvokeAiInstance: # NOTE: currently the config script does its own arg parsing! this means the command-line switches # from the installer will also automatically propagate down to the config script. # this may change in the future with config refactoring! - invokeai_configure.main() + succeeded = False + try: + invokeai_configure.main() + succeeded = True + except ConnectionError as e: + print(f'A network error was encountered during configuration and download: {str(e)}') + except OSError as e: + print(f'An OS error was encountered during configuration and download: {str(e)}') + except Exception as e: + print(f'A problem was encountered during the configuration and download steps: {str(e)}') + finally: + if not succeeded: + print('You may be able to finish the process by launching "invoke.sh" or "invoke.bat"') + print('from within the "invokeai" directory, and choosing options 5 and/or 6.') + print('Alternatively you can relaunch the installer.') def install_user_scripts(self): """ diff --git a/ldm/invoke/config/invokeai_configure.py b/ldm/invoke/config/invokeai_configure.py index da03c2bc5b..f9aab0c2f1 100755 --- a/ldm/invoke/config/invokeai_configure.py +++ b/ldm/invoke/config/invokeai_configure.py @@ -8,7 +8,6 @@ # print("Loading Python libraries...\n") import argparse -import curses import io import os import re @@ -19,6 +18,7 @@ import warnings from argparse import Namespace from pathlib import Path from urllib import request +from shutil import get_terminal_size import npyscreen import torch @@ -46,7 +46,8 @@ from .model_install_backend import ( recommended_datasets, hf_download_with_resume, ) -from .widgets import IntTitleSlider, CenteredButtonPress +from .widgets import IntTitleSlider, CenteredButtonPress, set_min_terminal_size + warnings.filterwarnings("ignore") @@ -64,6 +65,10 @@ SD_Configs = Path(global_config_dir()) / "stable-diffusion" Datasets = OmegaConf.load(Dataset_path) +# minimum size for the UI +MIN_COLS = 120 +MIN_LINES = 45 + INIT_FILE_PREAMBLE = """# 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 @@ -109,8 +114,6 @@ Add the '--help' argument to see all of the command-line switches available for # --------------------------------------------- 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: @@ -332,8 +335,7 @@ class editOptsForm(npyscreen.FormMultiPage): old_opts = self.parentApp.invokeai_opts first_time = not (Globals.root / Globals.initfile).exists() access_token = HfFolder.get_token() - - window_height, window_width = curses.initscr().getmaxyx() + window_width,window_height = get_terminal_size() for i in [ "Configure startup settings. You can come back and change these later.", "Use ctrl-N and ctrl-P to move to the ext and

revious fields.", @@ -676,6 +678,8 @@ def run_console_ui( ) -> (Namespace, Namespace): # parse_args() will read from init file if present invokeai_opts = default_startup_options(initfile) + + set_min_terminal_size(MIN_COLS, MIN_LINES) editApp = EditOptApplication(program_opts, invokeai_opts) editApp.run() if editApp.user_cancelled: @@ -683,7 +687,6 @@ def run_console_ui( else: return (editApp.new_opts, editApp.user_selections) - # ------------------------------------- def write_opts(opts: Namespace, init_file: Path): """ diff --git a/ldm/invoke/config/model_install.py b/ldm/invoke/config/model_install.py index bfd94e06de..8fbff94051 100644 --- a/ldm/invoke/config/model_install.py +++ b/ldm/invoke/config/model_install.py @@ -10,7 +10,6 @@ The work is actually done in backend code in model_install_backend.py. """ import argparse -import curses import os import sys import traceback @@ -22,15 +21,23 @@ import npyscreen import torch from npyscreen import widget from omegaconf import OmegaConf +from shutil import get_terminal_size from ..devices import choose_precision, choose_torch_device from ..globals import Globals, global_config_dir from .model_install_backend import (Dataset_path, default_config_file, default_dataset, get_root, install_requested_models, - recommended_datasets) + recommended_datasets, + ) from .widgets import (MultiSelectColumns, TextBox, - OffsetButtonPress, CenteredTitleText) + OffsetButtonPress, CenteredTitleText, + set_min_terminal_size, + ) + +# minimum size for the UI +MIN_COLS = 120 +MIN_LINES = 45 class addModelsForm(npyscreen.FormMultiPage): # for responsive resizing - disabled @@ -50,7 +57,7 @@ class addModelsForm(npyscreen.FormMultiPage): super().__init__(parentApp=parentApp, name=name, *args, **keywords) def create(self): - window_height, window_width = curses.initscr().getmaxyx() + window_width, window_height = get_terminal_size() starter_model_labels = self._get_starter_model_labels() recommended_models = [ x @@ -249,7 +256,7 @@ class addModelsForm(npyscreen.FormMultiPage): ) def _get_starter_model_labels(self) -> List[str]: - window_height, window_width = curses.initscr().getmaxyx() + window_width, window_height = get_terminal_size() label_width = 25 checkbox_width = 4 spacing_width = 2 @@ -268,7 +275,7 @@ class addModelsForm(npyscreen.FormMultiPage): ] def _get_columns(self) -> int: - window_height, window_width = curses.initscr().getmaxyx() + window_width, window_height = get_terminal_size() cols = ( 4 if window_width > 240 @@ -362,7 +369,6 @@ class AddModelApplication(npyscreen.NPSAppManaged): "MAIN", addModelsForm, name="Install Stable Diffusion Models" ) - # -------------------------------------------------------- def process_and_execute(opt: Namespace, selections: Namespace): models_to_remove = [ @@ -409,6 +415,7 @@ def select_and_download_models(opt: Namespace): precision=precision, ) else: + set_min_terminal_size(MIN_COLS, MIN_LINES) installApp = AddModelApplication() installApp.run() diff --git a/ldm/invoke/config/model_install_backend.py b/ldm/invoke/config/model_install_backend.py index d948747b9c..3cd63ef31f 100644 --- a/ldm/invoke/config/model_install_backend.py +++ b/ldm/invoke/config/model_install_backend.py @@ -67,6 +67,9 @@ def install_requested_models( purge_deleted: bool = False, config_file_path: Path = None, ): + ''' + Entry point for installing/deleting starter models, or installing external models. + ''' config_file_path=config_file_path or default_config_file() if not config_file_path.exists(): open(config_file_path,'w') @@ -124,7 +127,7 @@ def install_requested_models( output.writelines([line]) output.writelines([f'{argument} {str(scan_directory)}']) os.replace(replacement,initfile) - + # ------------------------------------- def yes_or_no(prompt: str, default_yes=True): default = "y" if default_yes else "n" diff --git a/ldm/invoke/config/widgets.py b/ldm/invoke/config/widgets.py index 1fbb5fbce0..e96c01fb4e 100644 --- a/ldm/invoke/config/widgets.py +++ b/ldm/invoke/config/widgets.py @@ -2,8 +2,34 @@ Widget class definitions used by model_select.py, merge_diffusers.py and textual_inversion.py ''' import math +import platform import npyscreen +import sys import curses +import termios +import struct +import fcntl + +from shutil import get_terminal_size + +# ------------------------------------- +def set_terminal_size(columns: int, lines: int): + os = platform.uname().system + if os=="Windows": + os.system(f'mode con: cols={columns} lines={lines}') + elif os in ['Darwin', 'Linux']: + winsize = struct.pack("HHHH", lines, columns, 0, 0) + fcntl.ioctl(0, termios.TIOCSWINSZ, winsize) + sys.stdout.write("\x1b[8;{rows};{cols}t".format(rows=lines, cols=columns)) + sys.stdout.flush() + + +def set_min_terminal_size(min_cols: int, min_lines: int): + # make sure there's enough room for the ui + term_cols, term_lines = get_terminal_size() + cols = max(term_cols, min_cols) + lines = max(term_lines, min_lines) + set_terminal_size(cols,lines) class IntSlider(npyscreen.Slider): def translate_value(self): @@ -23,7 +49,6 @@ class CenteredTitleText(npyscreen.TitleText): maxy, maxx = self.parent.curses_pad.getmaxyx() label = self.name self.relx = (maxx - len(label)) // 2 - begin_entry_at = -self.relx + 2 # ------------------------------------- class CenteredButtonPress(npyscreen.ButtonPress):