diff --git a/.github/workflows/test-invoke-conda.yml b/.github/workflows/test-invoke-conda.yml index ddf618c6e4..6560cb4930 100644 --- a/.github/workflows/test-invoke-conda.yml +++ b/.github/workflows/test-invoke-conda.yml @@ -116,7 +116,7 @@ jobs: - name: run configure_invokeai.py id: run-preload-models run: | - python scripts/configure_invokeai.py --no-interactive --yes + python scripts/configure_invokeai.py --skip-sd-weights --yes - name: cat invokeai.init id: cat-invokeai diff --git a/.github/workflows/test-invoke-pip.yml b/.github/workflows/test-invoke-pip.yml index 01e061f63e..5d6a7b4bbe 100644 --- a/.github/workflows/test-invoke-pip.yml +++ b/.github/workflows/test-invoke-pip.yml @@ -118,7 +118,7 @@ jobs: - name: run configure_invokeai.py id: run-preload-models - run: python3 scripts/configure_invokeai.py --no-interactive --yes + run: python3 scripts/configure_invokeai.py --skip-sd-weights --yes - name: Run the tests id: run-tests diff --git a/docs/features/CONCEPTS.md b/docs/features/CONCEPTS.md index f92e52f9e0..440dbb12d8 100644 --- a/docs/features/CONCEPTS.md +++ b/docs/features/CONCEPTS.md @@ -43,6 +43,22 @@ You can also combine styles and concepts: ## Using a Hugging Face Concept +!!! warning "Authenticating to HuggingFace" + + Some concepts require valid authentication to HuggingFace. Without it, they will not be downloaded + and will be silently ignored. + + If you used an installer to install InvokeAI, you may have already set a HuggingFace token. + If you skipped this step, you can: + + - run the InvokeAI configuration script again (if you used a manual installer): `scripts/configure_invokeai.py` + - set one of the `HUGGINGFACE_TOKEN` or `HUGGING_FACE_HUB_TOKEN` environment variables to contain your token + + Finally, if you already used any HuggingFace library on your computer, you might already have a token + in your local cache. Check for a hidden `.huggingface` directory in your home folder. If it + contains a `token` file, then you are all set. + + Hugging Face TI concepts are downloaded and installed automatically as you require them. This requires your machine to be connected to the Internet. To find out what each concept is for, you can browse the diff --git a/docs/installation/020_INSTALL_MANUAL.md b/docs/installation/020_INSTALL_MANUAL.md index 94246652a2..fa0ec8e41d 100644 --- a/docs/installation/020_INSTALL_MANUAL.md +++ b/docs/installation/020_INSTALL_MANUAL.md @@ -459,12 +459,12 @@ greatest version, launch the Anaconda window, enter `InvokeAI` and type: ```bash git pull conda env update -python scripts/configure_invokeai.py --no-interactive #optional +python scripts/configure_invokeai.py --skip-sd-weights #optional ``` This will bring your local copy into sync with the remote one. The last step may be needed to take advantage of new features or released models. The -`--no-interactive` flag will prevent the script from prompting you to download +`--skip-sd-weights` flag will prevent the script from prompting you to download the big Stable Diffusion weights files. ## Troubleshooting diff --git a/ldm/invoke/CLI.py b/ldm/invoke/CLI.py index 6fc441aac6..bf4949a1fb 100644 --- a/ldm/invoke/CLI.py +++ b/ldm/invoke/CLI.py @@ -110,7 +110,7 @@ def main(): max_loaded_models=opt.max_loaded_models, ) except (FileNotFoundError, TypeError, AssertionError): - emergency_model_reconfigure() + emergency_model_reconfigure(opt) sys.exit(-1) except (IOError, KeyError) as e: print(f'{e}. Aborting.') @@ -123,7 +123,7 @@ def main(): try: gen.load_model() except AssertionError: - emergency_model_reconfigure() + emergency_model_reconfigure(opt) sys.exit(-1) # web server loops forever @@ -939,7 +939,7 @@ def write_commands(opt, file_path:str, outfilepath:str): f.write('\n'.join(commands)) print(f'>> File {outfilepath} with commands created') -def emergency_model_reconfigure(): +def emergency_model_reconfigure(opt): print() print('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') print(' You appear to have a missing or misconfigured model file(s). ') @@ -948,11 +948,17 @@ def emergency_model_reconfigure(): print('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') print('configure_invokeai is launching....\n') - sys.argv = [ - 'configure_invokeai', - os.environ.get( - 'INVOKE_MODEL_RECONFIGURE', - '--interactive')] + # Match arguments that were set on the CLI + # only the arguments accepted by the configuration script are parsed + root_dir = ["--root", opt.root_dir] if opt.root_dir is not None else [] + config = ["--config", opt.conf] if opt.conf is not None else [] + yes_to_all = os.environ.get('INVOKE_MODEL_RECONFIGURE') + + sys.argv = [ 'configure_invokeai' ] + sys.argv.extend(root_dir) + sys.argv.extend(config) + if yes_to_all is not None: + sys.argv.append(yes_to_all) + import configure_invokeai configure_invokeai.main() - diff --git a/scripts/configure_invokeai.py b/scripts/configure_invokeai.py old mode 100644 new mode 100755 index dfc8579f3d..4158782ba3 --- a/scripts/configure_invokeai.py +++ b/scripts/configure_invokeai.py @@ -10,13 +10,14 @@ print('Loading Python libraries...\n') import argparse import sys import os +import io import re import warnings import shutil from urllib import request from tqdm import tqdm from omegaconf import OmegaConf -from huggingface_hub import HfFolder, hf_hub_url +from huggingface_hub import HfFolder, hf_hub_url, login as hf_hub_login from pathlib import Path from typing import Union from getpass_asterisk import getpass_asterisk @@ -191,61 +192,110 @@ def all_datasets()->dict: datasets[ds]=True return datasets +#--------------------------------------------- +def HfLogin(access_token) -> str: + """ + Helper for logging in to Huggingface + The stdout capture is needed to hide the irrelevant "git credential helper" warning + """ + + capture = io.StringIO() + sys.stdout = capture + try: + hf_hub_login(token = access_token, add_to_git_credential=False) + sys.stdout = sys.__stdout__ + except Exception as exc: + sys.stdout = sys.__stdout__ + print(exc) + raise exc + #-------------------------------Authenticate against Hugging Face -def authenticate(): +def authenticate(yes_to_all=False): + print('** LICENSE AGREEMENT FOR WEIGHT FILES **') + print("=" * os.get_terminal_size()[0]) print(''' -To download the Stable Diffusion weight files from the official Hugging Face -repository, you need to read and accept the CreativeML Responsible AI license. +By downloading the Stable Diffusion weight files from the official Hugging Face +repository, you agree to have read and accepted the CreativeML Responsible AI License. +The license terms are located here: -This involves a few easy steps. + https://huggingface.co/spaces/CompVis/stable-diffusion-license -1. If you have not already done so, create an account on Hugging Face's web site - using the "Sign Up" button: +''') + print("=" * os.get_terminal_size()[0]) - https://huggingface.co/join - - You will need to verify your email address as part of the HuggingFace - registration process. - -2. Log into your Hugging Face account: - - https://huggingface.co/login - -3. Accept the license terms located here: - - https://huggingface.co/runwayml/stable-diffusion-v1-5 - - and here: - - https://huggingface.co/runwayml/stable-diffusion-inpainting - - (Yes, you have to accept two slightly different license agreements) -''' - ) - input('Press when you are ready to continue:') - print('(Fetching Hugging Face token from cache...',end='') - access_token = HfFolder.get_token() - if access_token is not None: - print('found') + if not yes_to_all: + accepted = False + while not accepted: + accepted = yes_or_no('Accept the above License terms?') + if not accepted: + print('Please accept the License or Ctrl+C to exit.') + else: + print('Thank you!') else: - print('not found') + print("The program was started with a '--yes' flag, which indicates user's acceptance of the above License terms.") + + # Authenticate to Huggingface using environment variables. + # If successful, authentication will persist for either interactive or non-interactive use. + # Default env var expected by HuggingFace is HUGGING_FACE_HUB_TOKEN. + print("=" * os.get_terminal_size()[0]) + print('Authenticating to Huggingface') + hf_envvars = [ "HUGGING_FACE_HUB_TOKEN", "HUGGINGFACE_TOKEN" ] + if not (access_token := HfFolder.get_token()): + print(f"Huggingface token not found in cache.") + + for ev in hf_envvars: + if (access_token := os.getenv(ev)): + print(f"Token was found in the {ev} environment variable.... Logging in.") + try: + HfLogin(access_token) + continue + except ValueError: + print(f"Login failed due to invalid token found in {ev}") + else: + print(f"Token was not found in the environment variable {ev}.") + else: + print(f"Huggingface token found in cache.") + try: + HfLogin(access_token) + except ValueError: + print(f"Login failed due to invalid token found in cache") + + if not yes_to_all: print(''' -4. Thank you! The last step is to enter your HuggingFace access token so that - this script is authorized to initiate the download. Go to the access tokens - page of your Hugging Face account and create a token by clicking the - "New token" button: +You may optionally enter your Huggingface token now. InvokeAI *will* work without it, but some functionality may be limited. +See https://invoke-ai.github.io/InvokeAI/features/CONCEPTS/#using-a-hugging-face-concept for more information. - https://huggingface.co/settings/tokens +Visit https://huggingface.co/settings/tokens to generate a token. (Sign up for an account if needed). - (You can enter anything you like in the token creation field marked "Name". - "Role" should be "read"). +Paste the token below using Ctrl-Shift-V (macOS/Linux) or right-click (Windows), and/or 'Enter' to continue. +You may re-run the configuration script again in the future if you do not wish to set the token right now. + ''') + again = True + while again: + try: + access_token = getpass_asterisk.getpass_asterisk(prompt="HF Token ⮞ ") + HfLogin(access_token) + access_token = HfFolder.get_token() + again = False + except ValueError: + again = yes_or_no('Failed to log in to Huggingface. Would you like to try again?') + if not again: + print('\nRe-run the configuration script whenever you wish to set the token.') + print('...Continuing...') + except EOFError: + # this happens if the user pressed Enter on the prompt without any input; assume this means they don't want to input a token + # safety net needed against accidental "Enter"? + print("None provided - continuing") + again = False + + elif access_token is None: + print() + print("HuggingFace login did not succeed. Some functionality may be limited; see https://invoke-ai.github.io/InvokeAI/features/CONCEPTS/#using-a-hugging-face-concept for more information") + print() + print(f"Re-run the configuration script without '--yes' to set the HuggingFace token interactively, or use one of the environment variables: {', '.join(hf_envvars)}") + + print("=" * os.get_terminal_size()[0]) - Now copy the token to your clipboard and paste it at the prompt. Windows - users can paste with right-click or Ctrl-Shift-V. - Token: ''' - ) - access_token = getpass_asterisk.getpass_asterisk() - HfFolder.save_token(access_token) return access_token #--------------------------------------------- @@ -537,25 +587,14 @@ def download_safety_checker(): #------------------------------------- def download_weights(opt:dict) -> Union[str, None]: - # Authenticate to Huggingface using environment variables. - # If successful, authentication will persist for either interactive or non-interactive use. - # Default env var expected by HuggingFace is HUGGING_FACE_HUB_TOKEN. - if not (access_token := HfFolder.get_token()): - # If unable to find an existing token or expected environment, try the non-canonical environment variable (widely used in the community and supported as per docs) - if (access_token := os.getenv("HUGGINGFACE_TOKEN")): - # set the environment variable here instead of simply calling huggingface_hub.login(token), to maintain consistent behaviour. - # when calling the .login() method, the token is cached in the user's home directory. When the env var is used, the token is NOT cached. - os.environ['HUGGING_FACE_HUB_TOKEN'] = access_token if opt.yes_to_all: models = recommended_datasets() - if len(models)>0 and access_token is not None: + access_token = authenticate(opt.yes_to_all) + if len(models)>0: successfully_downloaded = download_weight_datasets(models, access_token) update_config_file(successfully_downloaded,opt) return - else: - print('** Cannot download models because no Hugging Face access token could be found. Please re-run without --yes') - return "could not download model weights from Huggingface due to missing or invalid access token" else: choice = user_wants_to_download_weights() @@ -571,11 +610,12 @@ def download_weights(opt:dict) -> Union[str, None]: else: # 'skip' return - print('** LICENSE AGREEMENT FOR WEIGHT FILES **') - # We are either already authenticated, or will be asked to provide the token interactively + access_token = authenticate() + print('\n** DOWNLOADING WEIGHTS **') successfully_downloaded = download_weight_datasets(models, access_token) + update_config_file(successfully_downloaded,opt) if len(successfully_downloaded) < len(models): return "some of the model weights downloads were not successful" @@ -695,7 +735,12 @@ def main(): dest='interactive', action=argparse.BooleanOptionalAction, default=True, - help='run in interactive mode (default)') + help='run in interactive mode (default) - DEPRECATED') + parser.add_argument('--skip-sd-weights', + dest='skip_sd_weights', + action=argparse.BooleanOptionalAction, + default=False, + help='skip downloading the large Stable Diffusion weight files') parser.add_argument('--yes','-y', dest='yes_to_all', action='store_true', @@ -728,7 +773,12 @@ def main(): # Optimistically try to download all required assets. If any errors occur, add them and proceed anyway. errors=set() - if opt.interactive: + if not opt.interactive: + print("WARNING: The --(no)-interactive argument is deprecated and will be removed. Use --skip-sd-weights.") + opt.skip_sd_weights=True + if opt.skip_sd_weights: + print('** SKIPPING DIFFUSION WEIGHTS DOWNLOAD PER USER REQUEST **') + else: print('** DOWNLOADING DIFFUSION WEIGHTS **') errors.add(download_weights(opt)) print('\n** DOWNLOADING SUPPORT MODELS **') diff --git a/setup.py b/setup.py index 4ae036bd84..23d26020f9 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,5 @@ import os import re - from setuptools import setup, find_packages def list_files(directory):