diff --git a/docs/features/NSFW.md b/docs/features/NSFW.md new file mode 100644 index 0000000000..9a39fd09c3 --- /dev/null +++ b/docs/features/NSFW.md @@ -0,0 +1,89 @@ +--- +title: The NSFW Checker +--- + +# :material-image-off: NSFW Checker + +## The NSFW ("Safety") Checker + +The Stable Diffusion image generation models will produce sexual +imagery if deliberately prompted, and will occasionally produce such +images when this is not intended. Such images are colloquially known +as "Not Safe for Work" (NSFW). This behavior is due to the nature of +the training set that Stable Diffusion was trained on, which culled +millions of "aesthetic" images from the Internet. + +You may not wish to be exposed to these images, and in some +jurisdictions it may be illegal to publicly distribute such imagery, +including mounting a publicly-available server that provides +unfiltered images to the public. Furthermore, the [Stable Diffusion +weights +License](https://github.com/invoke-ai/InvokeAI/blob/main/LICENSE-ModelWeights.txt) +forbids the model from being used to "exploit any of the +vulnerabilities of a specific group of persons." + +For these reasons Stable Diffusion offers a "safety checker," a +machine learning model trained to recognize potentially disturbing +imagery. When a potentially NSFW image is detected, the checker will +blur the image and paste a warning icon on top. The checker can be +turned on and off on the command line using `--nsfw_checker` and +`--no-nsfw_checker`. + +At installation time, InvokeAI will ask whether the checker should be +activated by default (neither argument given on the command line). The +response is stored in the InvokeAI initialization file (usually +`.invokeai` in your home directory). You can change the default at any +time by opening this file in a text editor and commenting or +uncommenting the line `--nsfw_checker`. + +## Caveats + +There are a number of caveats that you need to be aware of. + +### Accuracy + +The checker is [not perfect](https://arxiv.org/abs/2210.04610).It will +occasionally flag innocuous images (false positives), and will +frequently miss violent and gory imagery (false negatives). It rarely +fails to flag sexual imagery, but this has been known to happen. For +these reasons, the InvokeAI team prefers to refer to the software as a +"NSFW Checker" rather than "safety checker." + +### Memory Usage and Performance + +The NSFW checker consumes an additional 1.2G of GPU VRAM on top of the +3.4G of VRAM used by Stable Diffusion v1.5 (this is with +half-precision arithmetic). This means that the checker will not run +successfully on GPU cards with less than 6GB VRAM, and will reduce the +size of the images that you can produce. + +The checker also introduces a slight performance penalty. Images will +take ~1 second longer to generate when the checker is +activated. Generally this is not noticeable. + +### Intermediate Images in the Web UI + +The checker only operates on the final image produced by the Stable +Diffusion algorithm. If you are using the Web UI and have enabled the +display of intermediate images, you will briefly be exposed to a +low-resolution (mosaicized) version of the final image before it is +flagged by the checker and replaced by a fully blurred version. You +are encouraged to turn **off** intermediate image rendering when you +are using the checker. Future versions of InvokeAI will apply +additional blurring to intermediate images when the checker is active. + +### Watermarking + +InvokeAI does not apply any sort of watermark to images it +generates. However, it does write metadata into the PNG data area, +including the prompt used to generate the image and relevant parameter +settings. These fields can be examined using the `sd-metadata.py` +script that comes with the InvokeAI package. + +Note that several other Stable Diffusion distributions offer +wavelet-based "invisible" watermarking. We have experimented with the +library used to generate these watermarks and have reached the +conclusion that while the watermarking library may be adding +watermarks to PNG images, the currently available version is unable to +retrieve them successfully. If and when a functioning version of the +library becomes available, we will offer this feature as well. diff --git a/ldm/invoke/args.py b/ldm/invoke/args.py index 9fd6d730ef..e746e5bab3 100644 --- a/ldm/invoke/args.py +++ b/ldm/invoke/args.py @@ -468,7 +468,7 @@ class Args(object): action=argparse.BooleanOptionalAction, dest='safety_checker', default=False, - help='Check for and blur potentially NSFW images.', + help='Check for and blur potentially NSFW images. Use --no-nsfw_checker to disable.', ) model_group.add_argument( '--patchmatch', diff --git a/ldm/invoke/generator/base.py b/ldm/invoke/generator/base.py index ce199fefb1..ba3172e9dc 100644 --- a/ldm/invoke/generator/base.py +++ b/ldm/invoke/generator/base.py @@ -6,6 +6,7 @@ import torch import numpy as np import random import os +import os.path as osp import traceback from tqdm import tqdm, trange from PIL import Image, ImageFilter, ImageChops @@ -32,6 +33,7 @@ class Generator(): self.with_variations = [] self.use_mps_noise = False self.free_gpu_mem = None + self.caution_img = None # this is going to be overridden in img2img.py, txt2img.py and inpaint.py def get_make_image(self,prompt,**kwargs): @@ -290,13 +292,29 @@ class Generator(): def blur(self,input): blurry = input.filter(filter=ImageFilter.GaussianBlur(radius=32)) try: - caution = Image.open(CAUTION_IMG) - caution = caution.resize((caution.width // 2, caution.height //2)) - blurry.paste(caution,(0,0),caution) + caution = self.get_caution_img() + if caution: + blurry.paste(caution,(0,0),caution) except FileNotFoundError: pass return blurry + def get_caution_img(self): + if self.caution_img: + return self.caution_img + # Find the caution image. If we are installed in the package directory it will + # be six levels up. If we are in the repo directory it will be three levels up. + for dots in ('../../..','../../../../../..'): + caution_path = osp.join(osp.dirname(__file__),dots,CAUTION_IMG) + if osp.exists(caution_path): + path = caution_path + break + if not path: + return + caution = Image.open(path) + self.caution_img = caution.resize((caution.width // 2, caution.height //2)) + return self.caution_img + # this is a handy routine for debugging use. Given a generated sample, # convert it into a PNG image and store it at the indicated path def save_sample(self, sample, filepath): diff --git a/scripts/configure_invokeai.py b/scripts/configure_invokeai.py index fd7a009e66..2bfefaa28c 100644 --- a/scripts/configure_invokeai.py +++ b/scripts/configure_invokeai.py @@ -70,10 +70,10 @@ Web version: Command-line version: python scripts/invoke.py -Remember to activate that 'invokeai' environment before running invoke.py. - -Or, if you used one of the automated installers, execute "invoke.sh" (Linux/Mac) -or "invoke.bat" (Windows) to start the script. +If you installed manually, remember to activate the 'invokeai' +environment before running invoke.py. If you installed using the +automated installation script, execute "invoke.sh" (Linux/Mac) or +"invoke.bat" (Windows) to start InvokeAI. Have fun! ''' @@ -243,10 +243,10 @@ def download_weight_datasets(models:dict, access_token:str): for mod in models.keys(): repo_id = Datasets[mod]['repo_id'] filename = Datasets[mod]['file'] - print(os.path.join(Globals.root,Model_dir,Weights_dir), file=sys.stderr) + dest = os.path.join(Globals.root,Model_dir,Weights_dir) success = hf_download_with_resume( repo_id=repo_id, - model_dir=os.path.join(Globals.root,Model_dir,Weights_dir), + model_dir=dest, model_name=filename, access_token=access_token ) @@ -494,12 +494,12 @@ def download_clipseg(): #------------------------------------- def download_safety_checker(): - print('Installing safety model for NSFW content detection...',file=sys.stderr) + print('Installing model for NSFW content detection...',file=sys.stderr) try: from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker from transformers import AutoFeatureExtractor except ModuleNotFoundError: - print('Error installing safety checker model:') + print('Error installing NSFW checker model:') print(traceback.format_exc()) return safety_model_id = "CompVis/stable-diffusion-safety-checker" @@ -520,6 +520,7 @@ def download_weights(opt:dict): return else: print('** Cannot download models because no Hugging Face access token could be found. Please re-run without --yes') + return else: choice = user_wants_to_download_weights() @@ -584,7 +585,7 @@ def select_outputs(root:str,yes_to_all:bool=False): #------------------------------------- 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"' + assert os.path.exists('./configs'),'Run this script from within the InvokeAI source code directory, "InvokeAI" or the runtime directory "invokeai".' print(f'** INITIALIZING INVOKEAI RUNTIME DIRECTORY **') root_selected = False @@ -603,19 +604,50 @@ def initialize_rootdir(root:str,yes_to_all:bool=False): print(f'\nYou may change the chosen directories at any time by editing the --root and --outdir options in "{Globals.initfile}",') print(f'You may also change the runtime directory by setting the environment variable INVOKEAI_ROOT.\n') + enable_safety_checker = True + default_sampler = 'k_heun' + default_steps = '20' # deliberately a string - see test below + + sampler_choices =['ddim','k_dpm_2_a','k_dpm_2','k_euler_a','k_euler','k_heun','k_lms','plms'] + + if not yes_to_all: + print('The NSFW (not safe for work) checker blurs out images that potentially contain sexual imagery.') + print('It can be selectively enabled at run time with --nsfw_checker, and disabled with --no-nsfw_checker.') + print('The following option will set whether the checker is enabled by default. Like other options, you can') + print(f'change this setting later by editing the file {Globals.initfile}.') + enable_safety_checker = yes_or_no('Enable the NSFW checker by default?',enable_safety_checker) + + print('\nThe next choice selects the sampler to use by default. Samplers have different speed/performance') + print('tradeoffs. If you are not sure what to select, accept the default.') + sampler = None + while sampler not in sampler_choices: + sampler = input(f'Default sampler to use? ({", ".join(sampler_choices)}) [{default_sampler}]:') or default_sampler + + print('\nThe number of denoising steps affects both the speed and quality of the images generated.') + print('Higher steps often (but not always) increases the quality of the image, but increases image') + print('generation time. This can be changed at run time. Accept the default if you are unsure.') + steps = '' + while not steps.isnumeric(): + steps = input(f'Default number of steps to use during generation? [{default_steps}]:') or default_steps + else: + sampler = default_sampler + steps = default_steps + + safety_checker = '--nsfw_checker' if enable_safety_checker else '--no-nsfw_checker' + for name in ('models','configs','embeddings'): os.makedirs(os.path.join(root,name), exist_ok=True) for src in (['configs']): dest = os.path.join(root,src) if not os.path.samefile(src,dest): shutil.copytree(src,dest,dirs_exist_ok=True) - os.makedirs(outputs, exist_ok=True) + os.makedirs(outputs, exist_ok=True) init_file = os.path.expanduser(Globals.initfile) - if not os.path.exists(init_file): - print(f'Creating the initialization file at "{init_file}".\n') - with open(init_file,'w') as f: - f.write(f'''# InvokeAI initialization file + + 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. @@ -626,23 +658,18 @@ def initialize_rootdir(root:str,yes_to_all:bool=False): # the --outdir option controls the default location of image files. --outdir="{outputs}" +# generation arguments +{safety_checker} +--sampler={sampler} +--steps={steps} + # You may place other frequently-used startup commands here, one or more per line. # Examples: # --web --host=0.0.0.0 # --steps=20 # -Ak_euler_a -C10.0 # -''' - ) - else: - print(f'Updating the initialization file at "{init_file}".\n') - with open(init_file,'r') as infile, open(f'{init_file}.tmp','w') as outfile: - for line in infile.readlines(): - if not line.startswith('--root') and not line.startswith('--outdir'): - outfile.write(line) - outfile.write(f'--root="{root}"\n') - outfile.write(f'--outdir="{outputs}"\n') - os.replace(f'{init_file}.tmp',init_file) +''') #------------------------------------- class ProgressBar(): diff --git a/setup.py b/setup.py index 6c808c1f57..bfba829b4a 100644 --- a/setup.py +++ b/setup.py @@ -81,6 +81,7 @@ setup( 'scripts/preload_models.py', 'scripts/images2prompt.py','scripts/merge_embeddings.py' ], data_files=[('frontend/dist',list_files('frontend/dist')), - ('frontend/dist/assets',list_files('frontend/dist/assets')) + ('frontend/dist/assets',list_files('frontend/dist/assets')), + ('assets',['assets/caution.png']), ], )