multiple small fixes

1. Contents of autoscan directory field are restored after doing an installation.
2. Activate dialogue to choose V2 parameterization when importing from a directory.
3. Remove autoscan directory from init file when its checkbox is unselected.
4. Add widget cycling behavior to install models form.
This commit is contained in:
Lincoln Stein 2023-06-07 17:32:00 -04:00
parent a3357e073c
commit 9ed86a08f1
5 changed files with 136 additions and 120 deletions

View File

@ -13,6 +13,7 @@ import argparse
import io import io
import os import os
import shutil import shutil
import textwrap
import traceback import traceback
import warnings import warnings
from argparse import Namespace from argparse import Namespace
@ -321,11 +322,11 @@ class editOptsForm(CyclingForm, npyscreen.FormMultiPage):
first_time = not (config.root_path / 'invokeai.yaml').exists() first_time = not (config.root_path / 'invokeai.yaml').exists()
access_token = HfFolder.get_token() access_token = HfFolder.get_token()
window_width, window_height = get_terminal_size() window_width, window_height = get_terminal_size()
for i in [ label = """Configure startup settings. You can come back and change these later.
"Configure startup settings. You can come back and change these later.", Use ctrl-N and ctrl-P to move to the <N>ext and <P>revious fields.
"Use ctrl-N and ctrl-P to move to the <N>ext and <P>revious fields.", Use cursor arrows to make a checkbox selection, and space to toggle.
"Use cursor arrows to make a checkbox selection, and space to toggle.", """
]: for i in textwrap.wrap(label,width=window_width-6):
self.add_widget_intelligent( self.add_widget_intelligent(
npyscreen.FixedText, npyscreen.FixedText,
value=i, value=i,
@ -375,14 +376,13 @@ class editOptsForm(CyclingForm, npyscreen.FormMultiPage):
scroll_exit=True, scroll_exit=True,
) )
self.nextrely += 1 self.nextrely += 1
for i in [ label = """If you have an account at HuggingFace you may optionally paste your access token here
"If you have an account at HuggingFace you may optionally paste your access token here", to allow InvokeAI to download restricted styles & subjects from the "Concept Library". See https://huggingface.co/settings/tokens.
'to allow InvokeAI to download restricted styles & subjects from the "Concept Library".', """
"See https://huggingface.co/settings/tokens", for line in textwrap.wrap(label,width=window_width-6):
]:
self.add_widget_intelligent( self.add_widget_intelligent(
npyscreen.FixedText, npyscreen.FixedText,
value=i, value=line,
editable=False, editable=False,
color="CONTROL", color="CONTROL",
) )
@ -506,11 +506,11 @@ class editOptsForm(CyclingForm, npyscreen.FormMultiPage):
scroll_exit=True, scroll_exit=True,
) )
self.nextrely -= 1 self.nextrely -= 1
for i in [ label = """BY DOWNLOADING THE STABLE DIFFUSION WEIGHT FILES, YOU AGREE TO HAVE READ
"BY DOWNLOADING THE STABLE DIFFUSION WEIGHT FILES, YOU AGREE TO HAVE READ", AND ACCEPTED THE CREATIVEML RESPONSIBLE AI LICENSE LOCATED AT
"AND ACCEPTED THE CREATIVEML RESPONSIBLE AI LICENSE LOCATED AT", https://huggingface.co/spaces/CompVis/stable-diffusion-license
"https://huggingface.co/spaces/CompVis/stable-diffusion-license", """
]: for i in textwrap.wrap(label,width=window_width-6):
self.add_widget_intelligent( self.add_widget_intelligent(
npyscreen.FixedText, npyscreen.FixedText,
value=i, value=i,
@ -621,6 +621,7 @@ class EditOptApplication(npyscreen.NPSAppManaged):
addModelsForm, addModelsForm,
name="Install Stable Diffusion Models", name="Install Stable Diffusion Models",
multipage=True, multipage=True,
cycle_widgets=True,
) )
def new_opts(self): def new_opts(self):

View File

@ -157,6 +157,7 @@ def install_requested_models(
logger.info("INSTALLING EXTERNAL MODELS") logger.info("INSTALLING EXTERNAL MODELS")
for path_url_or_repo in external_models: for path_url_or_repo in external_models:
try: try:
logger.debug(f'In install_requested_models; callback = {model_config_file_callback}')
model_manager.heuristic_import( model_manager.heuristic_import(
path_url_or_repo, path_url_or_repo,
commit_to_conf=config_file_path, commit_to_conf=config_file_path,
@ -169,6 +170,8 @@ def install_requested_models(
if scan_at_startup and scan_directory.is_dir(): if scan_at_startup and scan_directory.is_dir():
update_autoconvert_dir(scan_directory) update_autoconvert_dir(scan_directory)
else:
update_autoconvert_dir(None)
def update_autoconvert_dir(autodir: Path): def update_autoconvert_dir(autodir: Path):
''' '''
@ -176,7 +179,7 @@ def update_autoconvert_dir(autodir: Path):
''' '''
invokeai_config_path = config.init_file_path invokeai_config_path = config.init_file_path
conf = OmegaConf.load(invokeai_config_path) conf = OmegaConf.load(invokeai_config_path)
conf.InvokeAI.Paths.autoconvert_dir = str(autodir) conf.InvokeAI.Paths.autoconvert_dir = str(autodir) if autodir else None
yaml = OmegaConf.to_yaml(conf) yaml = OmegaConf.to_yaml(conf)
tmpfile = invokeai_config_path.parent / "new_config.tmp" tmpfile = invokeai_config_path.parent / "new_config.tmp"
with open(tmpfile, "w", encoding="utf-8") as outfile: with open(tmpfile, "w", encoding="utf-8") as outfile:

View File

@ -321,6 +321,10 @@ class ModelManager(object):
for name in sorted(self.config, key=str.casefold): for name in sorted(self.config, key=str.casefold):
stanza = self.config[name] stanza = self.config[name]
with open('log.txt','a') as file:
print(f'DEBUG: name={name}; stanza = {stanza}',file=file)
# don't include VAEs in listing (legacy style) # don't include VAEs in listing (legacy style)
if "config" in stanza and "/VAE/" in stanza["config"]: if "config" in stanza and "/VAE/" in stanza["config"]:
continue continue
@ -820,7 +824,9 @@ class ModelManager(object):
Path(thing).rglob("*.safetensors") Path(thing).rglob("*.safetensors")
): ):
if model_name := self.heuristic_import( if model_name := self.heuristic_import(
str(m), commit_to_conf=commit_to_conf str(m),
commit_to_conf=commit_to_conf,
config_file_callback=config_file_callback,
): ):
self.logger.info(f"{model_name} successfully imported") self.logger.info(f"{model_name} successfully imported")
return model_name return model_name

View File

@ -14,6 +14,7 @@ import curses
import os import os
import sys import sys
import textwrap import textwrap
import traceback
from argparse import Namespace from argparse import Namespace
from multiprocessing import Process from multiprocessing import Process
from multiprocessing.connection import Connection, Pipe from multiprocessing.connection import Connection, Pipe
@ -128,10 +129,10 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage):
SingleSelectColumns, SingleSelectColumns,
values=[ values=[
'STARTER MODELS', 'STARTER MODELS',
'MORE DIFFUSION MODELS', 'MORE MODELS',
'CONTROLNET MODELS', 'CONTROLNETS',
'LORA/LYCORIS MODELS', 'LORA/LYCORIS',
'TEXTUAL INVERSION MODELS', 'TEXTUAL INVERSION',
], ],
value=[self.current_tab], value=[self.current_tab],
columns = 5, columns = 5,
@ -183,33 +184,28 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage):
BufferBox, BufferBox,
name='Log Messages', name='Log Messages',
editable=False, editable=False,
max_height = 20, max_height = 16,
) )
self.nextrely += 1 self.nextrely += 1
done_label = "INSTALL/REMOVE NOW" done_label = "APPLY CHANGES"
back_label = "BACK" back_label = "BACK"
button_length = len(done_label)
button_offset = 0
if self.multipage: if self.multipage:
button_length += len(back_label) + 1
button_offset += len(back_label) + 1
self.back_button = self.add_widget_intelligent( self.back_button = self.add_widget_intelligent(
npyscreen.ButtonPress, npyscreen.ButtonPress,
name=back_label, name=back_label,
relx=(window_width - button_length) // 2,
rely=-3, rely=-3,
when_pressed_function=self.on_back, when_pressed_function=self.on_back,
) )
self.ok_button = self.add_widget_intelligent( self.ok_button = self.add_widget_intelligent(
npyscreen.ButtonPress, # OffsetButtonPress, npyscreen.ButtonPress,
name=done_label, name=done_label,
relx=button_offset + 1 + (window_width - button_length) // 2, relx=(window_width - len(done_label)) // 2,
rely=-3, rely=-3,
when_pressed_function=self.on_execute when_pressed_function=self.on_execute
) )
label = "INSTALL AND EXIT" label = "APPLY CHANGES & EXIT"
self.done = self.add_widget_intelligent( self.done = self.add_widget_intelligent(
npyscreen.ButtonPress, npyscreen.ButtonPress,
name=label, name=label,
@ -289,13 +285,14 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage):
model_type: str, model_type: str,
window_width: int=120, window_width: int=120,
install_prompt: str=None, install_prompt: str=None,
add_purge_deleted: bool=False,
)->dict[str,npyscreen.widget]: )->dict[str,npyscreen.widget]:
'''Generic code to create model selection widgets''' '''Generic code to create model selection widgets'''
widgets = dict() widgets = dict()
model_list = sorted(predefined_models.keys()) model_list = sorted(predefined_models.keys())
if len(model_list) > 0: if len(model_list) > 0:
max_width = max([len(x) for x in model_list]) max_width = max([len(x) for x in model_list])
columns = window_width // (max_width+6) # 6 characters for "[x] " and padding columns = window_width // (max_width+8) # 8 characters for "[x] " and padding
columns = min(len(model_list),columns) or 1 columns = min(len(model_list),columns) or 1
prompt = install_prompt or f"Select the desired {model_type} models to install. Unchecked models will be purged from disk." prompt = install_prompt or f"Select the desired {model_type} models to install. Unchecked models will be purged from disk."
@ -325,26 +322,27 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage):
) )
) )
self.nextrely += 1 if add_purge_deleted:
widgets.update( self.nextrely += 1
label2 = self.add_widget_intelligent( widgets.update(
npyscreen.TitleFixedText, purge_deleted = self.add_widget_intelligent(
name="Additional URLs or HuggingFace repo_ids to install (Space separated. Use shift-control-V to paste):", npyscreen.Checkbox,
relx=4, name="Purge unchecked diffusers models from disk",
color='CONTROL', value=False,
editable=False, scroll_exit=True,
scroll_exit=True relx=4,
) )
) )
widgets['purge_deleted'].when_value_edited = lambda: self.sync_purge_buttons(widgets['purge_deleted'])
self.nextrely -= 1 self.nextrely += 1
widgets.update( widgets.update(
download_ids = self.add_widget_intelligent( download_ids = self.add_widget_intelligent(
TextBox, TextBox,
name = "Additional URLs, or HuggingFace repo_ids to install (Space separated. Use shift-control-V to paste):",
max_height=4, max_height=4,
scroll_exit=True, scroll_exit=True,
editable=True, editable=True,
relx=4
) )
) )
return widgets return widgets
@ -361,27 +359,18 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage):
predefined_models, predefined_models,
'Diffusers', 'Diffusers',
window_width, window_width,
install_prompt="Additional diffusers models already installed. Uncheck to purge from disk.", install_prompt="Additional diffusers models already installed.",
add_purge_deleted=True
) )
self.nextrely += 2
widgets.update(
purge_deleted = self.add_widget_intelligent(
npyscreen.Checkbox,
name="Purge unchecked diffusers models from disk",
value=False,
scroll_exit=True,
relx=4,
)
)
label = "Directory to scan for models to automatically import (<tab> autocompletes):" label = "Directory to scan for models to automatically import (<tab> autocompletes):"
self.nextrely += 2 self.nextrely += 1
widgets.update( widgets.update(
autoload_directory = self.add_widget_intelligent( autoload_directory = self.add_widget_intelligent(
# npyscreen.TitleFilename,
FileBox, FileBox,
max_height=3, max_height=3,
name=label, name=label,
value=str(config.autoconvert_dir) if config.autoconvert_dir else None,
select_dir=True, select_dir=True,
must_exist=True, must_exist=True,
use_two_lines=False, use_two_lines=False,
@ -394,12 +383,11 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage):
autoscan_on_startup = self.add_widget_intelligent( autoscan_on_startup = self.add_widget_intelligent(
npyscreen.Checkbox, npyscreen.Checkbox,
name="Scan and import from this directory each time InvokeAI starts", name="Scan and import from this directory each time InvokeAI starts",
value=False, value=config.autoconvert_dir is not None,
relx=4, relx=4,
scroll_exit=True, scroll_exit=True,
) )
) )
widgets['purge_deleted'].when_value_edited = lambda: self.sync_purge_buttons(widgets['purge_deleted'])
return widgets return widgets
def sync_purge_buttons(self,checkbox): def sync_purge_buttons(self,checkbox):
@ -557,15 +545,21 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage):
self.subprocess_connection = None self.subprocess_connection = None
self.monitor.entry_widget.buffer(['** Action Complete **']) self.monitor.entry_widget.buffer(['** Action Complete **'])
self.display() self.display()
# rebuild the form, saving log messages
# rebuild the form, saving and restoring some of the fields that need to be preserved.
saved_messages = self.monitor.entry_widget.values saved_messages = self.monitor.entry_widget.values
multipage = self.multipage autoload_dir = self.diffusers_models['autoload_directory'].value
autoscan = self.diffusers_models['autoscan_on_startup'].value
app.main_form = app.addForm( app.main_form = app.addForm(
"MAIN", addModelsForm, name="Install Stable Diffusion Models", multipage=multipage, "MAIN", addModelsForm, name="Install Stable Diffusion Models", multipage=self.multipage,
) )
app.switchForm("MAIN") app.switchForm("MAIN")
app.main_form.monitor.entry_widget.values = saved_messages app.main_form.monitor.entry_widget.values = saved_messages
app.main_form.monitor.entry_widget.buffer([''],scroll_end=True) app.main_form.monitor.entry_widget.buffer([''],scroll_end=True)
app.main_form.diffusers_models['autoload_directory'].value = autoload_dir
app.main_form.diffusers_models['autoscan_on_startup'].value = autoscan
############################################################### ###############################################################
@ -822,6 +816,7 @@ def select_and_download_models(opt: Namespace):
if do_listings(opt): if do_listings(opt):
pass pass
# this processes command line additions/removals
elif opt.diffusers or opt.controlnets or opt.textual_inversions or opt.loras: elif opt.diffusers or opt.controlnets or opt.textual_inversions or opt.loras:
action = 'remove_models' if opt.delete else 'install_models' action = 'remove_models' if opt.delete else 'install_models'
diffusers_args = {'diffusers':ModelInstallList(remove_models=opt.diffusers or [])} \ diffusers_args = {'diffusers':ModelInstallList(remove_models=opt.diffusers or [])} \
@ -846,6 +841,8 @@ def select_and_download_models(opt: Namespace):
diffusers=ModelInstallList(install_models=recommended_datasets()), diffusers=ModelInstallList(install_models=recommended_datasets()),
precision=precision, precision=precision,
) )
# this is where the TUI is called
else: else:
# needed because the torch library is loaded, even though we don't use it # needed because the torch library is loaded, even though we don't use it
torch.multiprocessing.set_start_method("spawn") torch.multiprocessing.set_start_method("spawn")
@ -856,12 +853,14 @@ def select_and_download_models(opt: Namespace):
installApp = AddModelApplication(opt) installApp = AddModelApplication(opt)
try: try:
installApp.run() installApp.run()
except KeyboardInterrupt: except KeyboardInterrupt as e:
form = installApp.main_form if hasattr(installApp,'main_form'):
if form.subprocess and form.subprocess.is_alive(): if installApp.main_form.subprocess \
logger.info('Terminating subprocesses') and installApp.main_form.subprocess.is_alive():
form.subprocess.terminate() logger.info('Terminating subprocesses')
form.subprocess = None installApp.main_form.subprocess.terminate()
installApp.main_form.subprocess = None
raise e
process_and_execute(opt, installApp.user_selections) process_and_execute(opt, installApp.user_selections)
# ------------------------------------- # -------------------------------------
@ -970,7 +969,8 @@ def main():
"Insufficient horizontal space for the interface. Please make your window wider and try again." "Insufficient horizontal space for the interface. Please make your window wider and try again."
) )
except Exception as e: except Exception as e:
print(f'An exception has occurred: {str(e)}') print(f'An exception has occurred: {str(e)} Details:')
print(traceback.format_exc(), file=sys.stderr)
input('Press any key to continue...') input('Press any key to continue...')

View File

@ -17,7 +17,7 @@ from shutil import get_terminal_size
from curses import BUTTON2_CLICKED,BUTTON3_CLICKED from curses import BUTTON2_CLICKED,BUTTON3_CLICKED
# minimum size for UIs # minimum size for UIs
MIN_COLS = 140 MIN_COLS = 120
MIN_LINES = 50 MIN_LINES = 50
# ------------------------------------- # -------------------------------------
@ -247,7 +247,7 @@ class SingleSelectColumns(SelectColumnBase, SingleSelectWithChanged):
def h_cursor_line_right(self,ch): def h_cursor_line_right(self,ch):
self.h_exit_down('bye bye') self.h_exit_down('bye bye')
class TextBox(npyscreen.MultiLineEdit): class TextBoxInner(npyscreen.MultiLineEdit):
def __init__(self,*args,**kwargs): def __init__(self,*args,**kwargs):
super().__init__(*args,**kwargs) super().__init__(*args,**kwargs)
@ -292,54 +292,57 @@ class TextBox(npyscreen.MultiLineEdit):
if bstate & (BUTTON2_CLICKED|BUTTON3_CLICKED): if bstate & (BUTTON2_CLICKED|BUTTON3_CLICKED):
self.h_paste() self.h_paste()
def update(self, clear=True): # def update(self, clear=True):
if clear: # if clear:
self.clear() # self.clear()
HEIGHT = self.height # HEIGHT = self.height
WIDTH = self.width # WIDTH = self.width
# draw box. # # draw box.
self.parent.curses_pad.hline(self.rely, self.relx, curses.ACS_HLINE, WIDTH) # self.parent.curses_pad.hline(self.rely, self.relx, curses.ACS_HLINE, WIDTH)
self.parent.curses_pad.hline( # self.parent.curses_pad.hline(
self.rely + HEIGHT, self.relx, curses.ACS_HLINE, WIDTH # self.rely + HEIGHT, self.relx, curses.ACS_HLINE, WIDTH
) # )
self.parent.curses_pad.vline( # self.parent.curses_pad.vline(
self.rely, self.relx, curses.ACS_VLINE, self.height # self.rely, self.relx, curses.ACS_VLINE, self.height
) # )
self.parent.curses_pad.vline( # self.parent.curses_pad.vline(
self.rely, self.relx + WIDTH, curses.ACS_VLINE, HEIGHT # self.rely, self.relx + WIDTH, curses.ACS_VLINE, HEIGHT
) # )
# draw corners # # draw corners
self.parent.curses_pad.addch( # self.parent.curses_pad.addch(
self.rely, # self.rely,
self.relx, # self.relx,
curses.ACS_ULCORNER, # curses.ACS_ULCORNER,
) # )
self.parent.curses_pad.addch( # self.parent.curses_pad.addch(
self.rely, # self.rely,
self.relx + WIDTH, # self.relx + WIDTH,
curses.ACS_URCORNER, # curses.ACS_URCORNER,
) # )
self.parent.curses_pad.addch( # self.parent.curses_pad.addch(
self.rely + HEIGHT, # self.rely + HEIGHT,
self.relx, # self.relx,
curses.ACS_LLCORNER, # curses.ACS_LLCORNER,
) # )
self.parent.curses_pad.addch( # self.parent.curses_pad.addch(
self.rely + HEIGHT, # self.rely + HEIGHT,
self.relx + WIDTH, # self.relx + WIDTH,
curses.ACS_LRCORNER, # curses.ACS_LRCORNER,
) # )
# fool our superclass into thinking drawing area is smaller - this is really hacky but it seems to work # # fool our superclass into thinking drawing area is smaller - this is really hacky but it seems to work
(relx, rely, height, width) = (self.relx, self.rely, self.height, self.width) # (relx, rely, height, width) = (self.relx, self.rely, self.height, self.width)
self.relx += 1 # self.relx += 1
self.rely += 1 # self.rely += 1
self.height -= 1 # self.height -= 1
self.width -= 1 # self.width -= 1
super().update(clear=False) # super().update(clear=False)
(self.relx, self.rely, self.height, self.width) = (relx, rely, height, width) # (self.relx, self.rely, self.height, self.width) = (relx, rely, height, width)
class TextBox(npyscreen.BoxTitle):
_contained_widget = TextBoxInner
class BufferBox(npyscreen.BoxTitle): class BufferBox(npyscreen.BoxTitle):
_contained_widget = npyscreen.BufferPager _contained_widget = npyscreen.BufferPager
@ -354,6 +357,9 @@ class ConfirmCancelPopup(fmPopup.ActionPopup):
class FileBox(npyscreen.BoxTitle): class FileBox(npyscreen.BoxTitle):
_contained_widget = npyscreen.Filename _contained_widget = npyscreen.Filename
class PrettyTextBox(npyscreen.BoxTitle):
_contained_widget = TextBox
def _wrap_message_lines(message, line_length): def _wrap_message_lines(message, line_length):
lines = [] lines = []
for line in message.split('\n'): for line in message.split('\n'):