From 608b3c51b93ad12710f32cd3286f9a08ecee23f2 Mon Sep 17 00:00:00 2001 From: yun saki Date: Thu, 25 Aug 2022 02:50:27 +0200 Subject: [PATCH 01/14] first implementation, some things are still missing --- ldm/simplet2i.py | 52 +++++-- scripts/morph.py | 349 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 392 insertions(+), 9 deletions(-) create mode 100644 scripts/morph.py diff --git a/ldm/simplet2i.py b/ldm/simplet2i.py index 4737d90ba70..0990df19876 100644 --- a/ldm/simplet2i.py +++ b/ldm/simplet2i.py @@ -155,10 +155,27 @@ def __init__(self, self.seed = seed @torch.no_grad() - def txt2img(self,prompt,outdir=None,batch_size=None,iterations=None, - steps=None,seed=None,grid=None,individual=None,width=None,height=None, - cfg_scale=None,ddim_eta=None,strength=None,embedding_path=None,init_img=None, - skip_normalize=False,variants=None): # note the "variants" option is an unused hack caused by how options are passed + def txt2img( + self, + prompt, + outdir=None, + batch_size=None, + iterations=None, + steps=None, + seed=None, + grid=None, + individual=None, + width=None, + height=None, + cfg_scale=None, + ddim_eta=None, + strength=None, + embedding_path=None, + init_img=None, + skip_normalize=False, + repeats=None, # note the "repeats" option is an unused hack caused by how options are passed + feedback=None, # same goes for "feedback" + ): """ Generate an image from the prompt, writing iteration images into the outdir The output is a list of lists in the format: [[filename1,seed1], [filename2,seed2],...] @@ -172,7 +189,7 @@ def txt2img(self,prompt,outdir=None,batch_size=None,iterations=None, ddim_eta = ddim_eta or self.ddim_eta batch_size = batch_size or self.batch_size iterations = iterations or self.iterations - strength = strength or self.strength # not actually used here, but preserved for code refactoring + strength = strength or self.strength # not actually used here, but preserved for code refactoring embedding_path = embedding_path or self.embedding_path model = self.load_model() # will instantiate the model or return it from cache @@ -284,10 +301,27 @@ def txt2img(self,prompt,outdir=None,batch_size=None,iterations=None, # There is lots of shared code between this and txt2img and should be refactored. @torch.no_grad() - def img2img(self,prompt,outdir=None,init_img=None,batch_size=None,iterations=None, - steps=None,seed=None,grid=None,individual=None,width=None,height=None, - cfg_scale=None,ddim_eta=None,strength=None,embedding_path=None, - skip_normalize=False,variants=None): # note the "variants" option is an unused hack caused by how options are passed + def img2img( + self, + prompt, + outdir=None, + init_img=None, + batch_size=None, + iterations=None, + steps=None, + seed=None, + grid=None, + individual=None, + width=None, + height=None, + cfg_scale=None, + ddim_eta=None, + strength=None, + embedding_path=None, + skip_normalize=False, + repeats=None, # note the "repeats" option is an unused hack caused by how options are passed + feedback=None, # same goes for "feedback" + ): """ Generate an image from the prompt and the initial image, writing iteration images into the outdir The output is a list of lists in the format: [[filename1,seed1], [filename2,seed2],...] diff --git a/scripts/morph.py b/scripts/morph.py new file mode 100644 index 00000000000..3b06ef67476 --- /dev/null +++ b/scripts/morph.py @@ -0,0 +1,349 @@ +# Derived from source code carrying the following copyrights +# Copyright (c) 2022 Lincoln D. Stein (https://github.com/lstein) + +import argparse +import shlex +import atexit +import os +import sys +import copy +from PIL import Image, PngImagePlugin + +skip_load_model = False +t2i = None + +# check if readline is available +try: + import readline + readline_available = True +except ModuleNotFoundError: + readline_available = False + + +def init() -> None: + print("Setup...") + + # command line history will be stored in "~/.morph_history" + if readline_available: + init_readline() + + #sys.path.append('.') + + from pytorch_lightning import logging + from ldm.simplet2i import T2I + + # prevent warning message on frozen clip tokenizer + import transformers + transformers.logging.set_verbosity_error() + + argv_opts = parse_argv() + + if argv_opts.laion400m: + # defaults for older latent diffusion weights + width = 256 + height = 256 + config = "configs/latent-diffusion/txt2img-1p4B-eval.yaml" + weights = "models/ldm/text2img-large/model.ckpt" + else: + # defaults for stable diffusion + width = 512 + height = 512 + config = "configs/stable-diffusion/v1-inference.yaml" + weights = "models/ldm/stable-diffusion-v1/model.ckpt" + + # create text2image object with default parameters + # overridden in the user input loop + global t2i + t2i = T2I( + height=height, + batch_size=argv_opts.batch_size, + outdir=argv_opts.outdir, + sampler_name=argv_opts.sampler_name, + weights=weights, + full_precision=argv_opts.full_precision, + config=config, + latent_diffusion_weights=argv_opts.laion400m, + embedding_path=argv_opts.embedding_path, + device=argv_opts.device, + ) + + # set up logging + log_path = os.path.join(argv_opts.outdir, "morph_log.txt") + + # ensure output directory + if not os.path.exists(argv_opts.outdir): + os.makedirs(argv_opts.outdir) + + # supress random seed message + logging.getLogger("pytorch_lightning").setLevel(logging.ERROR) + + # load infile + infile_lines: list = None + if argv_opts.infile: + with open(argv_opts.infile, "r") as file: + infile = file.read() + infile = infile.split('\n') + + # preload model + try: + if skip_load_model: + print("######################") + print("# Model not loaded!! #") + print("######################") + else: + t2i.load_model() + except FileNotFoundError: + print(f"Cannot find weights at {weights}") + sys.exit(1) + + print("Initialisation complete.") + + cmd_parser = parse_cmd() + + #with open(log_path, 'a') as log: + # start user loop + done = False + while not done: + try: + done = user_loop(cmd_parser, infile_lines) + except KeyboardInterrupt: + done = True + + +def user_loop( + cmd_parser: argparse.ArgumentParser(), + infile_lines: list, +) -> bool: + # read command from interactive cli + if infile_lines is None: + command = input("morph> ") + + # read command from infile + else: + try: + command = infile_lines.pop(0) + # infile_lines is empty; terminate + except IndexError: + return True + + # skip empty lines + if not command.strip(): + return False + + # skip if command is a comment + if command.startswith(("#", "//")): + return False + + # escape single quotes + command = command.replace("'", "\\'") + + try: + elements = shlex.split(command) + except ValueError as e: + print(e) + return False + + # skip if elements are empty + if not elements: + return False + + # quit if first element is 'q' + if elements[0] == "q": + return True + + # change output directory + if elements[0] == "cd": + new_dir = change_dir(elements) + + # skip if new_dir is empty + if not new_dir: + return False + + t2i.outdir = new_dir + del new_dir + + # print output directory + if elements[0] == "pwd": + print(f"Current output directory: {t2i.outdir}") + return False + + # show help + if elements[0] == "help": + cmd_parser.print_help() + return False + + # remove '!dream' command + if elements[0] == "!dream": + elements.pop(0) + + # seperate prompt from dash arguments + args = [] + args_set = False + + for idx, element in enumerate(elements): + if element[0] == "-": + # element is a dash argument + args.append(" ".join(elements[:idx])) + args += elements[idx:] + args_set = True + break + + if not args_set: + args.append(" ".join(elements)) + + try: + cmd_opts = cmd_parser.parse_args(args) + except SystemExit: + return False + + if not cmd_opts.prompt: + print("Prompt required.") + return False + + generate(cmd_opts) + + return False + + +def generate(cmd_opts: argparse.Namespace) -> None: + results = [] + + for r in range(cmd_opts.repeats + 1): + t2i_args = eval_params(copy.deepcopy(vars(cmd_opts)), r) + + # in feedback mode: replaces the init_img with the first image from the last result + if cmd_opts.feedback: + t2i_args = {**t2i_args, "init_img": results[-1][0][0]} + + try: + if not cmd_opts.init_img: + results.append(t2i.txt2img(**t2i_args)) + else: + assert os.path.exists(opt.init_img), f"No file found at {cmd_opts.init_img}. On Linux systems, pressing after -I will autocomplete a list of possible image files." + + if cmd_opts.width or cmd_opts.height: + print("Warning: width and height options are ignored when modifying an init image") + + results.append(t2i.img2img(**t2i_args)) + + except AssertionError as e: + print(e) + return + + +def eval_params(t2i_args: vars, r: int) -> vars: + params = ("steps", "seed", "width", "height", "cfg_scale", "strength") + floats = ("cfg_scale", "strength") + + for p in params: + if not t2i_args[p]: + continue + + convert = float if p in floats else int + split = t2i_args[p].split(":") + if len(split) > 1: + t2i_args[p] = convert(split[0]) + r * convert(split[1]) + else: + t2i_args[p] = convert(split[0]) + + return t2i_args + + +def init_readline() -> None: + pass + + +def parse_argv() -> argparse.Namespace(): + parser = argparse.ArgumentParser() + add_arg = parser.add_argument + + add_arg("-l", "--laion400m", "--latent_diffusion", + dest="laion400m", + action="store_true", + help="fallback to the latent diffusion (laion400m) weights and config") + add_arg("--from_file", + dest="infile", + type=str, + help="if specified, load prompts from this file") + add_arg("-n", "--iterations", + type=int, + default=1, + help="number of images to generate") + add_arg("-F", "--full_precision", + dest="full_precision", + action="store_true", + help="use slower full precision math for calculations") + add_arg("-b", "--batch_size", + type=int, + default=1, + help="number of images to produce per iteration (faster, but doesn't generate individual seeds") + add_arg("--sampler", "-m", + dest="sampler_name", + choices=["ddim", "k_dpm_2_a", "k_dpm_2", "k_euler_a", "k_euler", "k_heun", "k_lms", "plms"], + default="k_lms", + help="which sampler to use (k_lms) - can only be set on command line") + add_arg("-o", "--outdir", + type=str, + default="outputs/img-samples", + help="directory in which to place generated images and a log of prompts and seeds") + add_arg("--embedding_path", + type=str, + help="Path to a pre-trained embedding manager checkpoint - can only be set on command line") + add_arg("-d", "--device", + type=str, + default="cuda", + help="device to run stable diffusion on. defaults to cuda `torch.cuda.current_device()` if avalible") + + return parser.parse_args() + + +def parse_cmd() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser() + add_arg = parser.add_argument + + add_arg("prompt") + add_arg("-s", "--steps", type=str, + help="number of steps") + add_arg("-S", "--seed", type=str, + help="image seed") + add_arg("-n", "--iterations", type=int, default=1, + help="number of samplings to perform (slower than batches, but will provide seeds for individual images)") + add_arg("-b", "--batch_size", type=int, default=1, + help="number of images to produce per sampling (will not provide seeds for individual images!)") + add_arg("-W", "--width", type=str, + help="image width, must be a multiple of 64") + add_arg("-H", "--height", type=str, + help="image height, must be a multiple of 64") + add_arg("-C", "--cfg_scale", type=str, default="7", + help="prompt configuration scale") + add_arg("-g", "--grid", action="store_true", + help="generate a grid") + add_arg("-i", "--individual", action="store_true", + help="generate individual files (default)") + add_arg("-I", "--init_img", type=str, + help="path to input image for img2img mode (supersedes width and height)") + add_arg("-f", "--strength", type=str, default="0.75", + help="strength for noising/unnoising. 0.0 preserves image exactly, 1.0 replaces it completely") + add_arg("-r", "--repeats", type=int, default=0, + help="number of times values are incremented") + add_arg("-F", "--feedback", action="store_true", + help="feeds the first generated image back into the next one as an init_img") + add_arg("-x", "--skip_normalize", action="store_true", + help="skip subprompt weight normalization") + + return parser + + +def change_dir(elements) -> str: + if len(elements) == 2: + d = elements[1] + if os.path.exists(d): + return d + print(f"Directory '{d} does not exist.'") + else: + print("Invalid number of arguments. Usage: cd ") + + +if __name__ == "__main__": + init() + From 3dfadb0c4dd387b06d6e550b16ac368a6be817de Mon Sep 17 00:00:00 2001 From: yunsaki <110024973+yunsaki@users.noreply.github.com> Date: Thu, 25 Aug 2022 01:17:06 +0000 Subject: [PATCH 02/14] add todo list --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 22c64472488..6c12481b2b5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ +# small to do list +reconstruct prompt + +prompt tp png (more metadata) + +more descriptive filenames (cfg, steps, etc...) + +readline setup + +logging + +prompt morphing with weighting + # Stable Diffusion Dream Script This is a fork of CompVis/stable-diffusion, the wonderful open source From 05a2e65a8ebd5a00e253ac4d31dc8175db570dbc Mon Sep 17 00:00:00 2001 From: yun saki Date: Thu, 25 Aug 2022 10:03:19 +0200 Subject: [PATCH 03/14] don't catch KeyboardInterrupt in ldm/simplet2i, because it makes things complicated with a 'repeat' value greater than 0 --- ldm/simplet2i.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/ldm/simplet2i.py b/ldm/simplet2i.py index 0990df19876..c2453d33f7d 100644 --- a/ldm/simplet2i.py +++ b/ldm/simplet2i.py @@ -288,9 +288,6 @@ def txt2img( batch_size=batch_size, iterations=iterations, outdir=outdir) - except KeyboardInterrupt: - print('*interrupted*') - print('Partial results will be returned; if --grid was requested, nothing will be returned.') except RuntimeError as e: print(str(e)) From 71e5fdea79948b440207cd03d41d15d52365c16d Mon Sep 17 00:00:00 2001 From: yun saki Date: Thu, 25 Aug 2022 10:34:29 +0200 Subject: [PATCH 04/14] remove necessity to include unused parameters in ldm/simplet2i --- ldm/simplet2i.py | 4 ---- scripts/morph.py | 37 ++++++++++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/ldm/simplet2i.py b/ldm/simplet2i.py index c2453d33f7d..f24cb314e0d 100644 --- a/ldm/simplet2i.py +++ b/ldm/simplet2i.py @@ -173,8 +173,6 @@ def txt2img( embedding_path=None, init_img=None, skip_normalize=False, - repeats=None, # note the "repeats" option is an unused hack caused by how options are passed - feedback=None, # same goes for "feedback" ): """ Generate an image from the prompt, writing iteration images into the outdir @@ -316,8 +314,6 @@ def img2img( strength=None, embedding_path=None, skip_normalize=False, - repeats=None, # note the "repeats" option is an unused hack caused by how options are passed - feedback=None, # same goes for "feedback" ): """ Generate an image from the prompt and the initial image, writing iteration images into the outdir diff --git a/scripts/morph.py b/scripts/morph.py index 3b06ef67476..e16ce3b55a1 100644 --- a/scripts/morph.py +++ b/scripts/morph.py @@ -3,7 +3,7 @@ import argparse import shlex -import atexit +#import atexit import os import sys import copy @@ -208,6 +208,9 @@ def user_loop( def generate(cmd_opts: argparse.Namespace) -> None: results = [] + # cmd_opts that are not to be given to t2i.txt2img and t2i.img2img + invalid_keys = ("repeats", "feedback") + for r in range(cmd_opts.repeats + 1): t2i_args = eval_params(copy.deepcopy(vars(cmd_opts)), r) @@ -217,7 +220,10 @@ def generate(cmd_opts: argparse.Namespace) -> None: try: if not cmd_opts.init_img: - results.append(t2i.txt2img(**t2i_args)) + results.append(t2i.txt2img( + # removes dictionary entries with 'invalid_keys' + **{k: v for k, v in t2i_args if k not in invalid_keys} + )) else: assert os.path.exists(opt.init_img), f"No file found at {cmd_opts.init_img}. On Linux systems, pressing after -I will autocomplete a list of possible image files." @@ -230,6 +236,31 @@ def generate(cmd_opts: argparse.Namespace) -> None: print(e) return + write_log(cmd_opts, results) + + +def write_log(cmd_opts: argparse.Namespace, results: list) -> None: + pass + + +def normalise_args(opts: argparse.Namespace) -> list: + args = [] + + args.append(f"\"{opts.prompt}\"") + args.append(f"-s {opts.steps or t2i.steps}") + args.append(f"-b {opts.batch_size or t2i.batch_size}") + args.append(f"-W {opts.width or t2i.width}") + args.append(f"-H {opts.height or t2i.height}") + args.append(f"-C {opts.cfg_scale or t2i.cfg_scale}") + args.append(f"-m {t2i.sampler_name}") + args.append(f"-r {opts.repeats}") + opts.feedback and args.append("-B") + opts.init_img and args.append(f"-I {opts.init_img}") + opts.strength and opt.init_img and args.append(f"-f {opts.strength}") + opts.full_precision and args.append("-F") + + return args + def eval_params(t2i_args: vars, r: int) -> vars: params = ("steps", "seed", "width", "height", "cfg_scale", "strength") @@ -326,7 +357,7 @@ def parse_cmd() -> argparse.ArgumentParser: help="strength for noising/unnoising. 0.0 preserves image exactly, 1.0 replaces it completely") add_arg("-r", "--repeats", type=int, default=0, help="number of times values are incremented") - add_arg("-F", "--feedback", action="store_true", + add_arg("-B", "--feedback", action="store_true", help="feeds the first generated image back into the next one as an init_img") add_arg("-x", "--skip_normalize", action="store_true", help="skip subprompt weight normalization") From b4c33208d9eed69e123c556bbf37133542741a5e Mon Sep 17 00:00:00 2001 From: yun saki Date: Thu, 25 Aug 2022 10:45:33 +0200 Subject: [PATCH 05/14] fix dictionary unpacking and KeyboardInterrupt handling --- scripts/morph.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/morph.py b/scripts/morph.py index e16ce3b55a1..4b9ceadf3eb 100644 --- a/scripts/morph.py +++ b/scripts/morph.py @@ -107,7 +107,7 @@ def init() -> None: try: done = user_loop(cmd_parser, infile_lines) except KeyboardInterrupt: - done = True + print("Task cancelled. Enter q if you want to quit.") def user_loop( @@ -222,7 +222,7 @@ def generate(cmd_opts: argparse.Namespace) -> None: if not cmd_opts.init_img: results.append(t2i.txt2img( # removes dictionary entries with 'invalid_keys' - **{k: v for k, v in t2i_args if k not in invalid_keys} + **{k: v for k, v in t2i_args.items() if k not in invalid_keys} )) else: assert os.path.exists(opt.init_img), f"No file found at {cmd_opts.init_img}. On Linux systems, pressing after -I will autocomplete a list of possible image files." From 000c9bc1fbe486f29bbb8b3e717a7c74d852016f Mon Sep 17 00:00:00 2001 From: yun saki Date: Thu, 25 Aug 2022 12:48:52 +0200 Subject: [PATCH 06/14] implemented logging --- README.md | 6 --- scripts/morph.py | 102 +++++++++++++++++++++++++++++++++++++---------- 2 files changed, 80 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 6c12481b2b5..195649eca02 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,8 @@ # small to do list reconstruct prompt -prompt tp png (more metadata) - -more descriptive filenames (cfg, steps, etc...) - readline setup -logging - prompt morphing with weighting # Stable Diffusion Dream Script diff --git a/scripts/morph.py b/scripts/morph.py index 4b9ceadf3eb..1a064671748 100644 --- a/scripts/morph.py +++ b/scripts/morph.py @@ -11,6 +11,7 @@ skip_load_model = False t2i = None +log_path = "" # check if readline is available try: @@ -68,6 +69,7 @@ def init() -> None: ) # set up logging + global log_path log_path = os.path.join(argv_opts.outdir, "morph_log.txt") # ensure output directory @@ -108,6 +110,8 @@ def init() -> None: done = user_loop(cmd_parser, infile_lines) except KeyboardInterrupt: print("Task cancelled. Enter q if you want to quit.") + except EOFError: + return True def user_loop( @@ -215,22 +219,22 @@ def generate(cmd_opts: argparse.Namespace) -> None: t2i_args = eval_params(copy.deepcopy(vars(cmd_opts)), r) # in feedback mode: replaces the init_img with the first image from the last result - if cmd_opts.feedback: + if cmd_opts.feedback and results: t2i_args = {**t2i_args, "init_img": results[-1][0][0]} + # applies invalid keys + t2i_args = {k: v for k, v in t2i_args.items() if k not in invalid_keys} + try: if not cmd_opts.init_img: - results.append(t2i.txt2img( - # removes dictionary entries with 'invalid_keys' - **{k: v for k, v in t2i_args.items() if k not in invalid_keys} - )) + results.append([img + [t2i_args] for img in t2i.txt2img(**t2i_args)]) else: assert os.path.exists(opt.init_img), f"No file found at {cmd_opts.init_img}. On Linux systems, pressing after -I will autocomplete a list of possible image files." if cmd_opts.width or cmd_opts.height: print("Warning: width and height options are ignored when modifying an init image") - results.append(t2i.img2img(**t2i_args)) + results.append([img + [t2i_args] for img in t2i.img2img(**t2i_args)]) except AssertionError as e: print(e) @@ -240,26 +244,80 @@ def generate(cmd_opts: argparse.Namespace) -> None: def write_log(cmd_opts: argparse.Namespace, results: list) -> None: - pass + log_message = [] + + last_seed = None + img_num = 1 + batch_size = cmd_opts.batch_size or t2i.batch_size + seen = [] + + seeds = [img[1] for result in results for img in result] + if batch_size > 1: + seeds = f"(seeds for each batch row: {seeds})" + else: + seeds = f"(seeds for individual images: {seeds})" + + for repeat in results: + for result in repeat: + seed = result[1] + prompt_str = normalise_args(result[2]) + log_message.append(f"# {result[0]}: {prompt_str} -S {seed}") + + if batch_size > 1: + if seed != lasts_seed: + img_num = 1 + else: + img_num += 1 + + log_message[-1] += f" # (batch image {img_num} of {batch_size})" + last_seed = seed + print(log_message[-1]) + log_message[-1] += "\n" -def normalise_args(opts: argparse.Namespace) -> list: + if result[0] not in seen: + seen.append(result[0]) + + try: + if cmd_opts.grid: + write_prompt_to_png(result[0], f"{prompt_str} -g -S {seed} {seeds}") + else: + write_prompt_to_png(result[0], f"{prompt_str} -S {seed}") + except FileNotFoundError: + print(f"Could not open file '{result[0]}' for reading.") + + log_message = [normalise_args(vars(cmd_opts)) + "\n"] + log_message + print("Prompt:", log_message[0], end="") + + with open(log_path, "a") as file: + file.writelines(log_message) + + +def normalise_args(opts: dict) -> str: args = [] - args.append(f"\"{opts.prompt}\"") - args.append(f"-s {opts.steps or t2i.steps}") - args.append(f"-b {opts.batch_size or t2i.batch_size}") - args.append(f"-W {opts.width or t2i.width}") - args.append(f"-H {opts.height or t2i.height}") - args.append(f"-C {opts.cfg_scale or t2i.cfg_scale}") - args.append(f"-m {t2i.sampler_name}") - args.append(f"-r {opts.repeats}") - opts.feedback and args.append("-B") - opts.init_img and args.append(f"-I {opts.init_img}") - opts.strength and opt.init_img and args.append(f"-f {opts.strength}") - opts.full_precision and args.append("-F") - - return args + args.append(f"\"{opts.get('prompt')}\"") + args.append(f"-s {opts.get('steps') or t2i.steps}") + args.append(f"-b {opts.get('batch_size') or t2i.batch_size}") + args.append(f"-W {opts.get('width') or t2i.width}") + args.append(f"-H {opts.get('height') or t2i.height}") + args.append(f"-C {opts.get('cfg_scale') or t2i.cfg_scale}") + #args.append(f"-m {t2i.sampler_name}") + opts.get("repeats") and args.append(f"-r {opts.get('repeats')}") + opts.get("feedback") and args.append("-B") + opts.get("init_img") and args.append(f"-I {opts.get('init_img')}") + opts.get("strength") and opts.get("init_img") and args.append(f"-f {opts.get('strength')}") + #t2i.full_precision and args.append("-F") + + return " ".join(args) + + +def write_prompt_to_png(path, prompt) -> None: + info = PngImagePlugin.PngInfo() + info.add_text("Dream", prompt) + + with Image.open(path) as img: + img.save(path, "PNG", pnginfo=info) def eval_params(t2i_args: vars, r: int) -> vars: From 72fcda96bdcfe531d83cb2f3228bee8d65b02e6f Mon Sep 17 00:00:00 2001 From: yun saki Date: Thu, 25 Aug 2022 12:49:36 +0200 Subject: [PATCH 07/14] update readme --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 195649eca02..0c92fb57a08 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ # small to do list -reconstruct prompt - readline setup prompt morphing with weighting From b68d68cf1b17a5343f5e211b9f7afc72d511b1be Mon Sep 17 00:00:00 2001 From: yun saki Date: Thu, 25 Aug 2022 13:34:44 +0200 Subject: [PATCH 08/14] added readline support --- scripts/morph.py | 97 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 91 insertions(+), 6 deletions(-) diff --git a/scripts/morph.py b/scripts/morph.py index 1a064671748..e54f6475d57 100644 --- a/scripts/morph.py +++ b/scripts/morph.py @@ -3,7 +3,7 @@ import argparse import shlex -#import atexit +import atexit import os import sys import copy @@ -338,10 +338,6 @@ def eval_params(t2i_args: vars, r: int) -> vars: return t2i_args -def init_readline() -> None: - pass - - def parse_argv() -> argparse.Namespace(): parser = argparse.ArgumentParser() add_arg = parser.add_argument @@ -416,7 +412,7 @@ def parse_cmd() -> argparse.ArgumentParser: add_arg("-r", "--repeats", type=int, default=0, help="number of times values are incremented") add_arg("-B", "--feedback", action="store_true", - help="feeds the first generated image back into the next one as an init_img") + help="feeds the first generated image back into the next one as an init_img") add_arg("-x", "--skip_normalize", action="store_true", help="skip subprompt weight normalization") @@ -432,6 +428,95 @@ def change_dir(elements) -> str: else: print("Invalid number of arguments. Usage: cd ") +if readline_available: + def init_readline() -> None: + readline.set_completer(Completer( + ["cd", "pwd", "help", "q"] + ).complete) + readline.set_completer_delims(" ") + readline.parse_and_bind("tab: complete") + load_history() + + def load_history() -> None: + histfile = os.path.join(os.path.expanduser("~"), ".morph_history") + + try: + readline.read_history_file(histfile) + readline.set_history_length(1000) + except FileNotFoundError: + pass + + atexit.register(readline.write_history_file, histfile) + + class Completer(): + def __init__(self, options: list): + self.options = options + list(parse_cmd()._option_string_actions.keys()) + + def complete(self, text, state): + buffer = readline.get_line_buffer() + + if text.startswith(("-I", "--init_img")): + return self._path_completions(text, state, (".png")) + + if buffer.strip().endswith("cd") or text.startswith((".", "/")): + return self._path_completions(text, state, ()) + + response = None + + if state == 0: + # This is the first time for this text, so build a match list. + if text: + self.matches = [ + s for s in self.options if s and s.startswith(text) + ] + else: + self.matches = self.options[:] + + # Return state'th item from the match list, if we have that many. + try: + response = self.matches[state] + except IndexError: + response = None + + return response + + def _path_completions(self, text, state, extensions): + # get the path so far + if text.startswith("-I"): + path = text.replace("-I", "", 1).lstrip() + elif text.startswith("--init_img="): + path = text.replace("--init_img=", "", 1).lstrip() + else: + path = text + + matches = [] + path = os.path.expanduser(path) + + if not len(path): + matches.append(text + "./") + else: + directory = os.path.dirname(path) + dir_list = os.listdri(directory) + + for n in dir_list: + if n.startswith(".") and len(n): + continue + + full_path = os.path.join(directory, n) + + if full_path.startswith(path): + if os.path.isdir(full_path): + matches.append(os.path.join(os.path.dirname(text), n) + "/") + elif n.endswith(extensions): + matches.append(os.path.join(os.path.dirname(text), n)) + + try: + response = matches[state] + except IndexError: + response = None + + return response + if __name__ == "__main__": init() From 26ba8816c8343878afbeb4fe90e6c18676794f01 Mon Sep 17 00:00:00 2001 From: yun saki Date: Thu, 25 Aug 2022 13:36:11 +0200 Subject: [PATCH 09/14] fix formatting --- scripts/morph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/morph.py b/scripts/morph.py index e54f6475d57..5e0fbfc2548 100644 --- a/scripts/morph.py +++ b/scripts/morph.py @@ -412,7 +412,7 @@ def parse_cmd() -> argparse.ArgumentParser: add_arg("-r", "--repeats", type=int, default=0, help="number of times values are incremented") add_arg("-B", "--feedback", action="store_true", - help="feeds the first generated image back into the next one as an init_img") + help="feeds the first generated image back into the next one as an init_img") add_arg("-x", "--skip_normalize", action="store_true", help="skip subprompt weight normalization") From 1bfa9be208b94b80b65868d128cc72d5d1ff881b Mon Sep 17 00:00:00 2001 From: yun saki Date: Thu, 25 Aug 2022 13:42:45 +0200 Subject: [PATCH 10/14] reintroduced help message --- scripts/morph.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/morph.py b/scripts/morph.py index 5e0fbfc2548..7a492f29972 100644 --- a/scripts/morph.py +++ b/scripts/morph.py @@ -99,6 +99,7 @@ def init() -> None: sys.exit(1) print("Initialisation complete.") + print("('help' for help, 'q' to quit, 'cd' to change output dir, 'pwd' to print output dir)") cmd_parser = parse_cmd() From 0d80c928f70f9f712bb880f75f4148f105a499bd Mon Sep 17 00:00:00 2001 From: yun saki Date: Thu, 25 Aug 2022 14:09:54 +0200 Subject: [PATCH 11/14] reverted morph.py back to dream.py --- README.md | 5 - scripts/dream.py | 734 ++++++++++++++++++++++++++--------------------- scripts/morph.py | 524 --------------------------------- 3 files changed, 414 insertions(+), 849 deletions(-) mode change 100755 => 100644 scripts/dream.py delete mode 100644 scripts/morph.py diff --git a/README.md b/README.md index 8425f911576..a3347bdb4d8 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,3 @@ -# small to do list -readline setup - -prompt morphing with weighting - # Stable Diffusion Dream Script This is a fork of CompVis/stable-diffusion, the wonderful open source diff --git a/scripts/dream.py b/scripts/dream.py old mode 100755 new mode 100644 index e3efc4b3e0d..e379b9f750e --- a/scripts/dream.py +++ b/scripts/dream.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # Copyright (c) 2022 Lincoln D. Stein (https://github.com/lstein) import argparse @@ -7,423 +6,518 @@ import os import sys import copy -from PIL import Image,PngImagePlugin +from PIL import Image, PngImagePlugin -# readline unavailable on windows systems +skip_load_model = False +t2i = None +log_path = "" + +# check if readline is available try: import readline readline_available = True -except: +except ModuleNotFoundError: readline_available = False -debugging = False - -def main(): - ''' Initialize command-line parsers and the diffusion model ''' - arg_parser = create_argv_parser() - opt = arg_parser.parse_args() - if opt.laion400m: - # defaults suitable to the older latent diffusion weights - width = 256 - height = 256 - config = "configs/latent-diffusion/txt2img-1p4B-eval.yaml" - weights = "models/ldm/text2img-large/model.ckpt" - else: - # some defaults suitable for stable diffusion weights - width = 512 - height = 512 - config = "configs/stable-diffusion/v1-inference.yaml" - weights = "models/ldm/stable-diffusion-v1/model.ckpt" - # command line history will be stored in a file called "~/.dream_history" +def init() -> None: + print("Setup...") + + # command line history will be stored in "~/.dream_history" if readline_available: - setup_readline() + init_readline() + + #sys.path.append('.') - print("* Initializing, be patient...\n") - sys.path.append('.') from pytorch_lightning import logging from ldm.simplet2i import T2I - # these two lines prevent a horrible warning message from appearing - # when the frozen CLIP tokenizer is imported + + # prevent warning message on frozen clip tokenizer import transformers transformers.logging.set_verbosity_error() - - # creating a simple text2image object with a handful of - # defaults passed on the command line. - # additional parameters will be added (or overriden) during - # the user input loop - t2i = T2I(width=width, - height=height, - batch_size=opt.batch_size, - outdir=opt.outdir, - sampler_name=opt.sampler_name, - weights=weights, - full_precision=opt.full_precision, - config=config, - latent_diffusion_weights=opt.laion400m, # this is solely for recreating the prompt - embedding_path=opt.embedding_path, - device=opt.device - ) - # make sure the output directory exists - if not os.path.exists(opt.outdir): - os.makedirs(opt.outdir) - - # gets rid of annoying messages about random seed - logging.getLogger("pytorch_lightning").setLevel(logging.ERROR) + argv_opts = parse_argv() - infile = None - try: - if opt.infile is not None: - infile = open(opt.infile,'r') - except FileNotFoundError as e: - print(e) - exit(-1) + if argv_opts.laion400m: + # defaults for older latent diffusion weights + width = 256 + height = 256 + config = "configs/latent-diffusion/txt2img-1p4B-eval.yaml" + weights = "models/ldm/text2img-large/model.ckpt" + else: + # defaults for stable diffusion + width = 512 + height = 512 + config = "configs/stable-diffusion/v1-inference.yaml" + weights = "models/ldm/stable-diffusion-v1/model.ckpt" + + # create text2image object with default parameters + # overridden in the user input loop + global t2i + t2i = T2I( + height=height, + batch_size=argv_opts.batch_size, + outdir=argv_opts.outdir, + sampler_name=argv_opts.sampler_name, + weights=weights, + full_precision=argv_opts.full_precision, + config=config, + latent_diffusion_weights=argv_opts.laion400m, + embedding_path=argv_opts.embedding_path, + device=argv_opts.device, + ) + + # set up logging + global log_path + log_path = os.path.join(argv_opts.outdir, "dream_log.txt") - # preload the model - t2i.load_model() - print("\n* Initialization done! Awaiting your command (-h for help, 'q' to quit, 'cd' to change output dir, 'pwd' to print output dir)...") + # ensure output directory + if not os.path.exists(argv_opts.outdir): + os.makedirs(argv_opts.outdir) - log_path = os.path.join(opt.outdir,'dream_log.txt') - with open(log_path,'a') as log: - cmd_parser = create_cmd_parser() - main_loop(t2i,cmd_parser,log,infile) - log.close() - if infile: - infile.close() + # supress random seed message + logging.getLogger("pytorch_lightning").setLevel(logging.ERROR) + # load infile + infile_lines: list = None + if argv_opts.infile: + with open(argv_opts.infile, "r") as file: + infile = file.read() + infile = infile.split('\n') -def main_loop(t2i,parser,log,infile): - ''' prompt/read/execute loop ''' + # preload model + try: + if skip_load_model: + print("######################") + print("# Model not loaded!! #") + print("######################") + else: + t2i.load_model() + except FileNotFoundError: + print(f"Cannot find weights at {weights}") + sys.exit(1) + + print("Initialisation complete.") + print("('help' for help, 'q' to quit, 'cd' to change output dir, 'pwd' to print output dir)") + + cmd_parser = parse_cmd() + + #with open(log_path, 'a') as log: + # start user loop done = False - while not done: try: - command = infile.readline() if infile else input("dream> ") + done = user_loop(cmd_parser, infile_lines) + except KeyboardInterrupt: + print("Task cancelled. Enter q if you want to quit.") except EOFError: - done = True - break + return True - if infile and len(command)==0: - done = True - break - - if command.startswith(('#','//')): - continue - # before splitting, escape single quotes so as not to mess - # up the parser - command = command.replace("'","\\'") +def user_loop( + cmd_parser: argparse.ArgumentParser(), + infile_lines: list, +) -> bool: + # read command from interactive cli + if infile_lines is None: + command = input("dream> ") + # read command from infile + else: try: - elements = shlex.split(command) - except ValueError as e: - print(str(e)) - continue - - if len(elements)==0: - continue + command = infile_lines.pop(0) + # infile_lines is empty; terminate + except IndexError: + return True + + # skip empty lines + if not command.strip(): + return False + + # skip if command is a comment + if command.startswith(("#", "//")): + return False + + # escape single quotes + command = command.replace("'", "\\'") - if elements[0]=='q': - done = True + try: + elements = shlex.split(command) + except ValueError as e: + print(e) + return False + + # skip if elements are empty + if not elements: + return False + + # quit if first element is 'q' + if elements[0] == "q": + return True + + # change output directory + if elements[0] == "cd": + new_dir = change_dir(elements) + + # skip if new_dir is empty + if not new_dir: + return False + + t2i.outdir = new_dir + del new_dir + + # print output directory + if elements[0] == "pwd": + print(f"Current output directory: {t2i.outdir}") + return False + + # show help + if elements[0] == "help": + cmd_parser.print_help() + return False + + # remove '!dream' command + if elements[0] == "!dream": + elements.pop(0) + + # seperate prompt from dash arguments + args = [] + args_set = False + + for idx, element in enumerate(elements): + if element[0] == "-": + # element is a dash argument + args.append(" ".join(elements[:idx])) + args += elements[idx:] + args_set = True break - if elements[0]=='cd' and len(elements)>1: - if os.path.exists(elements[1]): - print(f"setting image output directory to {elements[1]}") - t2i.outdir=elements[1] - else: - print(f"directory {elements[1]} does not exist") - continue + if not args_set: + args.append(" ".join(elements)) - if elements[0]=='pwd': - print(f"current output directory is {t2i.outdir}") - continue - - if elements[0].startswith('!dream'): # in case a stored prompt still contains the !dream command - elements.pop(0) - - # rearrange the arguments to mimic how it works in the Dream bot. - switches = [''] - switches_started = False - - for el in elements: - if el[0]=='-' and not switches_started: - switches_started = True - if switches_started: - switches.append(el) - else: - switches[0] += el - switches[0] += ' ' - switches[0] = switches[0][:len(switches[0])-1] + try: + cmd_opts = cmd_parser.parse_args(args) + except SystemExit: + return False - try: - opt = parser.parse_args(switches) - except SystemExit: - parser.print_help() - continue - if len(opt.prompt)==0: - print("Try again with a prompt!") - continue + if not cmd_opts.prompt: + print("Prompt required.") + return False + + generate(cmd_opts) + + return False + + +def generate(cmd_opts: argparse.Namespace) -> None: + results = [] + + # cmd_opts that are not to be given to t2i.txt2img and t2i.img2img + invalid_keys = ("repeats", "feedback") + + for r in range(cmd_opts.repeats + 1): + t2i_args = eval_params(copy.deepcopy(vars(cmd_opts)), r) + + # in feedback mode: replaces the init_img with the first image from the last result + if cmd_opts.feedback and results: + t2i_args = {**t2i_args, "init_img": results[-1][0][0]} + + # applies invalid keys + t2i_args = {k: v for k, v in t2i_args.items() if k not in invalid_keys} try: - if opt.init_img is None: - results = t2i.txt2img(**vars(opt)) + if not cmd_opts.init_img: + results.append([img + [t2i_args] for img in t2i.txt2img(**t2i_args)]) else: - assert os.path.exists(opt.init_img),f"No file found at {opt.init_img}. On Linux systems, pressing after -I will autocomplete a list of possible image files." - if None not in (opt.width,opt.height): - print('Warning: width and height options are ignored when modifying an init image') - results = t2i.img2img(**vars(opt)) - except AssertionError as e: - print(e) - continue + assert os.path.exists(opt.init_img), f"No file found at {cmd_opts.init_img}. On Linux systems, pressing after -I will autocomplete a list of possible image files." + if cmd_opts.width or cmd_opts.height: + print("Warning: width and height options are ignored when modifying an init image") - allVariantResults = [] - if opt.variants is not None: - print(f"Generating {opt.variants} variant(s)...") - newopt = copy.deepcopy(opt) - newopt.iterations = 1 - newopt.variants = None - for r in results: - newopt.init_img = r[0] - print(f"\t generating variant for {newopt.init_img}") - for j in range(0, opt.variants): - try: - variantResults = t2i.img2img(**vars(newopt)) - allVariantResults.append([newopt,variantResults]) - except AssertionError as e: - print(e) - continue - print(f"{opt.variants} Variants generated!") + results.append([img + [t2i_args] for img in t2i.img2img(**t2i_args)]) - print("Outputs:") - write_log_message(t2i,opt,results,log) - - if allVariantResults: - print("Variant outputs:") - for vr in allVariantResults: - write_log_message(t2i,vr[0],vr[1],log) - + except AssertionError as e: + print(e) + return - print("goodbye!") + write_log(cmd_opts, results) -def write_log_message(t2i,opt,results,logfile): - ''' logs the name of the output image, its prompt and seed to the terminal, log file, and a Dream text chunk in the PNG metadata ''' - switches = _reconstruct_switches(t2i,opt) - prompt_str = ' '.join(switches) +def write_log(cmd_opts: argparse.Namespace, results: list) -> None: + log_message = [] - # when multiple images are produced in batch, then we keep track of where each starts - last_seed = None - img_num = 1 - batch_size = opt.batch_size or t2i.batch_size - seenit = {} + last_seed = None + img_num = 1 + batch_size = cmd_opts.batch_size or t2i.batch_size + seen = [] - seeds = [a[1] for a in results] + seeds = [img[1] for result in results for img in result] if batch_size > 1: seeds = f"(seeds for each batch row: {seeds})" else: seeds = f"(seeds for individual images: {seeds})" - for r in results: - seed = r[1] - log_message = (f'{r[0]}: {prompt_str} -S{seed}') + for repeat in results: + for result in repeat: + seed = result[1] + prompt_str = normalise_args(result[2]) + log_message.append(f"# {result[0]}: {prompt_str} -S {seed}") - if batch_size > 1: - if seed != last_seed: - img_num = 1 - log_message += f' # (batch image {img_num} of {batch_size})' - else: - img_num += 1 - log_message += f' # (batch image {img_num} of {batch_size})' - last_seed = seed - print(log_message) - logfile.write(log_message+"\n") - logfile.flush() - if r[0] not in seenit: - seenit[r[0]] = True - try: - if opt.grid: - _write_prompt_to_png(r[0],f'{prompt_str} -g -S{seed} {seeds}') + if batch_size > 1: + if seed != lasts_seed: + img_num = 1 else: - _write_prompt_to_png(r[0],f'{prompt_str} -S{seed}') - except FileNotFoundError: - print(f"Could not open file '{r[0]}' for reading") - -def _reconstruct_switches(t2i,opt): - '''Normalize the prompt and switches''' - switches = list() - switches.append(f'"{opt.prompt}"') - switches.append(f'-s{opt.steps or t2i.steps}') - switches.append(f'-b{opt.batch_size or t2i.batch_size}') - switches.append(f'-W{opt.width or t2i.width}') - switches.append(f'-H{opt.height or t2i.height}') - switches.append(f'-C{opt.cfg_scale or t2i.cfg_scale}') - switches.append(f'-m{t2i.sampler_name}') - if opt.variants: - switches.append(f'-v{opt.variants}') - if opt.init_img: - switches.append(f'-I{opt.init_img}') - if opt.strength and opt.init_img is not None: - switches.append(f'-f{opt.strength or t2i.strength}') - if t2i.full_precision: - switches.append('-F') - return switches - -def _write_prompt_to_png(path,prompt): + img_num += 1 + + log_message[-1] += f" # (batch image {img_num} of {batch_size})" + last_seed = seed + + print(log_message[-1]) + log_message[-1] += "\n" + + if result[0] not in seen: + seen.append(result[0]) + + try: + if cmd_opts.grid: + write_prompt_to_png(result[0], f"{prompt_str} -g -S {seed} {seeds}") + else: + write_prompt_to_png(result[0], f"{prompt_str} -S {seed}") + except FileNotFoundError: + print(f"Could not open file '{result[0]}' for reading.") + + log_message = [normalise_args(vars(cmd_opts)) + "\n"] + log_message + print("Prompt:", log_message[0], end="") + + with open(log_path, "a") as file: + file.writelines(log_message) + + +def normalise_args(opts: dict) -> str: + args = [] + + args.append(f"\"{opts.get('prompt')}\"") + args.append(f"-s {opts.get('steps') or t2i.steps}") + args.append(f"-b {opts.get('batch_size') or t2i.batch_size}") + args.append(f"-W {opts.get('width') or t2i.width}") + args.append(f"-H {opts.get('height') or t2i.height}") + args.append(f"-C {opts.get('cfg_scale') or t2i.cfg_scale}") + #args.append(f"-m {t2i.sampler_name}") + opts.get("repeats") and args.append(f"-r {opts.get('repeats')}") + opts.get("feedback") and args.append("-B") + opts.get("init_img") and args.append(f"-I {opts.get('init_img')}") + opts.get("strength") and opts.get("init_img") and args.append(f"-f {opts.get('strength')}") + #t2i.full_precision and args.append("-F") + + return " ".join(args) + + +def write_prompt_to_png(path, prompt) -> None: info = PngImagePlugin.PngInfo() - info.add_text("Dream",prompt) - im = Image.open(path) - im.save(path,"PNG",pnginfo=info) - -def create_argv_parser(): - parser = argparse.ArgumentParser(description="Parse script's command line args") - parser.add_argument("--laion400m", - "--latent_diffusion", - "-l", - dest='laion400m', - action='store_true', - help="fallback to the latent diffusion (laion400m) weights and config") - parser.add_argument("--from_file", - dest='infile', - type=str, - help="if specified, load prompts from this file") - parser.add_argument('-n','--iterations', - type=int, - default=1, - help="number of images to generate") - parser.add_argument('-F','--full_precision', - dest='full_precision', - action='store_true', - help="use slower full precision math for calculations") - parser.add_argument('-b','--batch_size', - type=int, - default=1, - help="number of images to produce per iteration (faster, but doesn't generate individual seeds") - parser.add_argument('--sampler','-m', - dest="sampler_name", - choices=['ddim', 'k_dpm_2_a', 'k_dpm_2', 'k_euler_a', 'k_euler', 'k_heun', 'k_lms', 'plms'], - default='k_lms', - help="which sampler to use (k_lms) - can only be set on command line") - parser.add_argument('--outdir', - '-o', - type=str, - default="outputs/img-samples", - help="directory in which to place generated images and a log of prompts and seeds") - parser.add_argument('--embedding_path', - type=str, - help="Path to a pre-trained embedding manager checkpoint - can only be set on command line") - parser.add_argument('--device', - '-d', - type=str, - default="cuda", - help="device to run stable diffusion on. defaults to cuda `torch.cuda.current_device()` if avalible") - return parser - - -def create_cmd_parser(): - parser = argparse.ArgumentParser(description='Example: dream> a fantastic alien landscape -W1024 -H960 -s100 -n12') - parser.add_argument('prompt') - parser.add_argument('-s','--steps',type=int,help="number of steps") - parser.add_argument('-S','--seed',type=int,help="image seed") - parser.add_argument('-n','--iterations',type=int,default=1,help="number of samplings to perform (slower, but will provide seeds for individual images)") - parser.add_argument('-b','--batch_size',type=int,default=1,help="number of images to produce per sampling (will not provide seeds for individual images!)") - parser.add_argument('-W','--width',type=int,help="image width, multiple of 64") - parser.add_argument('-H','--height',type=int,help="image height, multiple of 64") - parser.add_argument('-C','--cfg_scale',default=7.5,type=float,help="prompt configuration scale") - parser.add_argument('-g','--grid',action='store_true',help="generate a grid") - parser.add_argument('-i','--individual',action='store_true',help="generate individual files (default)") - parser.add_argument('-I','--init_img',type=str,help="path to input image for img2img mode (supersedes width and height)") - parser.add_argument('-f','--strength',default=0.75,type=float,help="strength for noising/unnoising. 0.0 preserves image exactly, 1.0 replaces it completely") - parser.add_argument('-v','--variants',type=int,help="in img2img mode, the first generated image will get passed back to img2img to generate the requested number of variants") - parser.add_argument('-x','--skip_normalize',action='store_true',help="skip subprompt weight normalization") + info.add_text("Dream", prompt) + + with Image.open(path) as img: + img.save(path, "PNG", pnginfo=info) + + +def eval_params(t2i_args: vars, r: int) -> vars: + params = ("steps", "seed", "width", "height", "cfg_scale", "strength") + floats = ("cfg_scale", "strength") + + for p in params: + if not t2i_args[p]: + continue + + convert = float if p in floats else int + split = t2i_args[p].split(":") + if len(split) > 1: + t2i_args[p] = convert(split[0]) + r * convert(split[1]) + else: + t2i_args[p] = convert(split[0]) + + return t2i_args + + +def parse_argv() -> argparse.Namespace(): + parser = argparse.ArgumentParser() + add_arg = parser.add_argument + + add_arg("-l", "--laion400m", "--latent_diffusion", + dest="laion400m", + action="store_true", + help="fallback to the latent diffusion (laion400m) weights and config") + add_arg("--from_file", + dest="infile", + type=str, + help="if specified, load prompts from this file") + add_arg("-n", "--iterations", + type=int, + default=1, + help="number of images to generate") + add_arg("-F", "--full_precision", + dest="full_precision", + action="store_true", + help="use slower full precision math for calculations") + add_arg("-b", "--batch_size", + type=int, + default=1, + help="number of images to produce per iteration (faster, but doesn't generate individual seeds") + add_arg("--sampler", "-m", + dest="sampler_name", + choices=["ddim", "k_dpm_2_a", "k_dpm_2", "k_euler_a", "k_euler", "k_heun", "k_lms", "plms"], + default="k_lms", + help="which sampler to use (k_lms) - can only be set on command line") + add_arg("-o", "--outdir", + type=str, + default="outputs/img-samples", + help="directory in which to place generated images and a log of prompts and seeds") + add_arg("--embedding_path", + type=str, + help="Path to a pre-trained embedding manager checkpoint - can only be set on command line") + add_arg("-d", "--device", + type=str, + default="cuda", + help="device to run stable diffusion on. defaults to cuda `torch.cuda.current_device()` if avalible") + + return parser.parse_args() + + +def parse_cmd() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser() + add_arg = parser.add_argument + + add_arg("prompt") + add_arg("-s", "--steps", type=str, + help="number of steps") + add_arg("-S", "--seed", type=str, + help="image seed") + add_arg("-n", "--iterations", type=int, default=1, + help="number of samplings to perform (slower than batches, but will provide seeds for individual images)") + add_arg("-b", "--batch_size", type=int, default=1, + help="number of images to produce per sampling (will not provide seeds for individual images!)") + add_arg("-W", "--width", type=str, + help="image width, must be a multiple of 64") + add_arg("-H", "--height", type=str, + help="image height, must be a multiple of 64") + add_arg("-C", "--cfg_scale", type=str, default="7", + help="prompt configuration scale") + add_arg("-g", "--grid", action="store_true", + help="generate a grid") + add_arg("-i", "--individual", action="store_true", + help="generate individual files (default)") + add_arg("-I", "--init_img", type=str, + help="path to input image for img2img mode (supersedes width and height)") + add_arg("-f", "--strength", type=str, default="0.75", + help="strength for noising/unnoising. 0.0 preserves image exactly, 1.0 replaces it completely") + add_arg("-r", "--repeats", type=int, default=0, + help="number of times values are incremented") + add_arg("-B", "--feedback", action="store_true", + help="feeds the first generated image back into the next one as an init_img") + add_arg("-x", "--skip_normalize", action="store_true", + help="skip subprompt weight normalization") + return parser + +def change_dir(elements) -> str: + if len(elements) == 2: + d = elements[1] + if os.path.exists(d): + return d + print(f"Directory '{d} does not exist.'") + else: + print("Invalid number of arguments. Usage: cd ") + if readline_available: - def setup_readline(): - readline.set_completer(Completer(['cd','pwd', - '--steps','-s','--seed','-S','--iterations','-n','--batch_size','-b', - '--width','-W','--height','-H','--cfg_scale','-C','--grid','-g', - '--individual','-i','--init_img','-I','--strength','-f','-v','--variants']).complete) + def init_readline() -> None: + readline.set_completer(Completer( + ["cd", "pwd", "help", "q"] + ).complete) readline.set_completer_delims(" ") - readline.parse_and_bind('tab: complete') + readline.parse_and_bind("tab: complete") load_history() - def load_history(): - histfile = os.path.join(os.path.expanduser('~'),".dream_history") + def load_history() -> None: + histfile = os.path.join(os.path.expanduser("~"), ".dream_history") + try: readline.read_history_file(histfile) readline.set_history_length(1000) except FileNotFoundError: pass - atexit.register(readline.write_history_file,histfile) + + atexit.register(readline.write_history_file, histfile) class Completer(): - def __init__(self,options): - self.options = sorted(options) - return + def __init__(self, options: list): + self.options = options + list(parse_cmd()._option_string_actions.keys()) - def complete(self,text,state): + def complete(self, text, state): buffer = readline.get_line_buffer() - - if text.startswith(('-I','--init_img')): - return self._path_completions(text,state,('.png')) - if buffer.strip().endswith('cd') or text.startswith(('.','/')): - return self._path_completions(text,state,()) + if text.startswith(("-I", "--init_img")): + return self._path_completions(text, state, (".png")) + + if buffer.strip().endswith("cd") or text.startswith((".", "/")): + return self._path_completions(text, state, ()) response = None + if state == 0: # This is the first time for this text, so build a match list. if text: - self.matches = [s - for s in self.options - if s and s.startswith(text)] + self.matches = [ + s for s in self.options if s and s.startswith(text) + ] else: self.matches = self.options[:] - # Return the state'th item from the match list, - # if we have that many. + # Return state'th item from the match list, if we have that many. try: response = self.matches[state] except IndexError: response = None + return response - def _path_completions(self,text,state,extensions): + def _path_completions(self, text, state, extensions): # get the path so far - if text.startswith('-I'): - path = text.replace('-I','',1).lstrip() - elif text.startswith('--init_img='): - path = text.replace('--init_img=','',1).lstrip() + if text.startswith("-I"): + path = text.replace("-I", "", 1).lstrip() + elif text.startswith("--init_img="): + path = text.replace("--init_img=", "", 1).lstrip() else: path = text - matches = list() - + matches = [] path = os.path.expanduser(path) - if len(path)==0: - matches.append(text+'./') + + if not len(path): + matches.append(text + "./") else: - dir = os.path.dirname(path) - dir_list = os.listdir(dir) + directory = os.path.dirname(path) + dir_list = os.listdri(directory) + for n in dir_list: - if n.startswith('.') and len(n)>1: + if n.startswith(".") and len(n): continue - full_path = os.path.join(dir,n) + + full_path = os.path.join(directory, n) + if full_path.startswith(path): if os.path.isdir(full_path): - matches.append(os.path.join(os.path.dirname(text),n)+'/') + matches.append(os.path.join(os.path.dirname(text), n) + "/") elif n.endswith(extensions): - matches.append(os.path.join(os.path.dirname(text),n)) + matches.append(os.path.join(os.path.dirname(text), n)) try: response = matches[state] except IndexError: response = None + return response + if __name__ == "__main__": - main() + init() diff --git a/scripts/morph.py b/scripts/morph.py deleted file mode 100644 index 7a492f29972..00000000000 --- a/scripts/morph.py +++ /dev/null @@ -1,524 +0,0 @@ -# Derived from source code carrying the following copyrights -# Copyright (c) 2022 Lincoln D. Stein (https://github.com/lstein) - -import argparse -import shlex -import atexit -import os -import sys -import copy -from PIL import Image, PngImagePlugin - -skip_load_model = False -t2i = None -log_path = "" - -# check if readline is available -try: - import readline - readline_available = True -except ModuleNotFoundError: - readline_available = False - - -def init() -> None: - print("Setup...") - - # command line history will be stored in "~/.morph_history" - if readline_available: - init_readline() - - #sys.path.append('.') - - from pytorch_lightning import logging - from ldm.simplet2i import T2I - - # prevent warning message on frozen clip tokenizer - import transformers - transformers.logging.set_verbosity_error() - - argv_opts = parse_argv() - - if argv_opts.laion400m: - # defaults for older latent diffusion weights - width = 256 - height = 256 - config = "configs/latent-diffusion/txt2img-1p4B-eval.yaml" - weights = "models/ldm/text2img-large/model.ckpt" - else: - # defaults for stable diffusion - width = 512 - height = 512 - config = "configs/stable-diffusion/v1-inference.yaml" - weights = "models/ldm/stable-diffusion-v1/model.ckpt" - - # create text2image object with default parameters - # overridden in the user input loop - global t2i - t2i = T2I( - height=height, - batch_size=argv_opts.batch_size, - outdir=argv_opts.outdir, - sampler_name=argv_opts.sampler_name, - weights=weights, - full_precision=argv_opts.full_precision, - config=config, - latent_diffusion_weights=argv_opts.laion400m, - embedding_path=argv_opts.embedding_path, - device=argv_opts.device, - ) - - # set up logging - global log_path - log_path = os.path.join(argv_opts.outdir, "morph_log.txt") - - # ensure output directory - if not os.path.exists(argv_opts.outdir): - os.makedirs(argv_opts.outdir) - - # supress random seed message - logging.getLogger("pytorch_lightning").setLevel(logging.ERROR) - - # load infile - infile_lines: list = None - if argv_opts.infile: - with open(argv_opts.infile, "r") as file: - infile = file.read() - infile = infile.split('\n') - - # preload model - try: - if skip_load_model: - print("######################") - print("# Model not loaded!! #") - print("######################") - else: - t2i.load_model() - except FileNotFoundError: - print(f"Cannot find weights at {weights}") - sys.exit(1) - - print("Initialisation complete.") - print("('help' for help, 'q' to quit, 'cd' to change output dir, 'pwd' to print output dir)") - - cmd_parser = parse_cmd() - - #with open(log_path, 'a') as log: - # start user loop - done = False - while not done: - try: - done = user_loop(cmd_parser, infile_lines) - except KeyboardInterrupt: - print("Task cancelled. Enter q if you want to quit.") - except EOFError: - return True - - -def user_loop( - cmd_parser: argparse.ArgumentParser(), - infile_lines: list, -) -> bool: - # read command from interactive cli - if infile_lines is None: - command = input("morph> ") - - # read command from infile - else: - try: - command = infile_lines.pop(0) - # infile_lines is empty; terminate - except IndexError: - return True - - # skip empty lines - if not command.strip(): - return False - - # skip if command is a comment - if command.startswith(("#", "//")): - return False - - # escape single quotes - command = command.replace("'", "\\'") - - try: - elements = shlex.split(command) - except ValueError as e: - print(e) - return False - - # skip if elements are empty - if not elements: - return False - - # quit if first element is 'q' - if elements[0] == "q": - return True - - # change output directory - if elements[0] == "cd": - new_dir = change_dir(elements) - - # skip if new_dir is empty - if not new_dir: - return False - - t2i.outdir = new_dir - del new_dir - - # print output directory - if elements[0] == "pwd": - print(f"Current output directory: {t2i.outdir}") - return False - - # show help - if elements[0] == "help": - cmd_parser.print_help() - return False - - # remove '!dream' command - if elements[0] == "!dream": - elements.pop(0) - - # seperate prompt from dash arguments - args = [] - args_set = False - - for idx, element in enumerate(elements): - if element[0] == "-": - # element is a dash argument - args.append(" ".join(elements[:idx])) - args += elements[idx:] - args_set = True - break - - if not args_set: - args.append(" ".join(elements)) - - try: - cmd_opts = cmd_parser.parse_args(args) - except SystemExit: - return False - - if not cmd_opts.prompt: - print("Prompt required.") - return False - - generate(cmd_opts) - - return False - - -def generate(cmd_opts: argparse.Namespace) -> None: - results = [] - - # cmd_opts that are not to be given to t2i.txt2img and t2i.img2img - invalid_keys = ("repeats", "feedback") - - for r in range(cmd_opts.repeats + 1): - t2i_args = eval_params(copy.deepcopy(vars(cmd_opts)), r) - - # in feedback mode: replaces the init_img with the first image from the last result - if cmd_opts.feedback and results: - t2i_args = {**t2i_args, "init_img": results[-1][0][0]} - - # applies invalid keys - t2i_args = {k: v for k, v in t2i_args.items() if k not in invalid_keys} - - try: - if not cmd_opts.init_img: - results.append([img + [t2i_args] for img in t2i.txt2img(**t2i_args)]) - else: - assert os.path.exists(opt.init_img), f"No file found at {cmd_opts.init_img}. On Linux systems, pressing after -I will autocomplete a list of possible image files." - - if cmd_opts.width or cmd_opts.height: - print("Warning: width and height options are ignored when modifying an init image") - - results.append([img + [t2i_args] for img in t2i.img2img(**t2i_args)]) - - except AssertionError as e: - print(e) - return - - write_log(cmd_opts, results) - - -def write_log(cmd_opts: argparse.Namespace, results: list) -> None: - log_message = [] - - last_seed = None - img_num = 1 - batch_size = cmd_opts.batch_size or t2i.batch_size - seen = [] - - seeds = [img[1] for result in results for img in result] - if batch_size > 1: - seeds = f"(seeds for each batch row: {seeds})" - else: - seeds = f"(seeds for individual images: {seeds})" - - for repeat in results: - for result in repeat: - seed = result[1] - prompt_str = normalise_args(result[2]) - log_message.append(f"# {result[0]}: {prompt_str} -S {seed}") - - if batch_size > 1: - if seed != lasts_seed: - img_num = 1 - else: - img_num += 1 - - log_message[-1] += f" # (batch image {img_num} of {batch_size})" - last_seed = seed - - print(log_message[-1]) - log_message[-1] += "\n" - - if result[0] not in seen: - seen.append(result[0]) - - try: - if cmd_opts.grid: - write_prompt_to_png(result[0], f"{prompt_str} -g -S {seed} {seeds}") - else: - write_prompt_to_png(result[0], f"{prompt_str} -S {seed}") - except FileNotFoundError: - print(f"Could not open file '{result[0]}' for reading.") - - log_message = [normalise_args(vars(cmd_opts)) + "\n"] + log_message - print("Prompt:", log_message[0], end="") - - with open(log_path, "a") as file: - file.writelines(log_message) - - -def normalise_args(opts: dict) -> str: - args = [] - - args.append(f"\"{opts.get('prompt')}\"") - args.append(f"-s {opts.get('steps') or t2i.steps}") - args.append(f"-b {opts.get('batch_size') or t2i.batch_size}") - args.append(f"-W {opts.get('width') or t2i.width}") - args.append(f"-H {opts.get('height') or t2i.height}") - args.append(f"-C {opts.get('cfg_scale') or t2i.cfg_scale}") - #args.append(f"-m {t2i.sampler_name}") - opts.get("repeats") and args.append(f"-r {opts.get('repeats')}") - opts.get("feedback") and args.append("-B") - opts.get("init_img") and args.append(f"-I {opts.get('init_img')}") - opts.get("strength") and opts.get("init_img") and args.append(f"-f {opts.get('strength')}") - #t2i.full_precision and args.append("-F") - - return " ".join(args) - - -def write_prompt_to_png(path, prompt) -> None: - info = PngImagePlugin.PngInfo() - info.add_text("Dream", prompt) - - with Image.open(path) as img: - img.save(path, "PNG", pnginfo=info) - - -def eval_params(t2i_args: vars, r: int) -> vars: - params = ("steps", "seed", "width", "height", "cfg_scale", "strength") - floats = ("cfg_scale", "strength") - - for p in params: - if not t2i_args[p]: - continue - - convert = float if p in floats else int - split = t2i_args[p].split(":") - if len(split) > 1: - t2i_args[p] = convert(split[0]) + r * convert(split[1]) - else: - t2i_args[p] = convert(split[0]) - - return t2i_args - - -def parse_argv() -> argparse.Namespace(): - parser = argparse.ArgumentParser() - add_arg = parser.add_argument - - add_arg("-l", "--laion400m", "--latent_diffusion", - dest="laion400m", - action="store_true", - help="fallback to the latent diffusion (laion400m) weights and config") - add_arg("--from_file", - dest="infile", - type=str, - help="if specified, load prompts from this file") - add_arg("-n", "--iterations", - type=int, - default=1, - help="number of images to generate") - add_arg("-F", "--full_precision", - dest="full_precision", - action="store_true", - help="use slower full precision math for calculations") - add_arg("-b", "--batch_size", - type=int, - default=1, - help="number of images to produce per iteration (faster, but doesn't generate individual seeds") - add_arg("--sampler", "-m", - dest="sampler_name", - choices=["ddim", "k_dpm_2_a", "k_dpm_2", "k_euler_a", "k_euler", "k_heun", "k_lms", "plms"], - default="k_lms", - help="which sampler to use (k_lms) - can only be set on command line") - add_arg("-o", "--outdir", - type=str, - default="outputs/img-samples", - help="directory in which to place generated images and a log of prompts and seeds") - add_arg("--embedding_path", - type=str, - help="Path to a pre-trained embedding manager checkpoint - can only be set on command line") - add_arg("-d", "--device", - type=str, - default="cuda", - help="device to run stable diffusion on. defaults to cuda `torch.cuda.current_device()` if avalible") - - return parser.parse_args() - - -def parse_cmd() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser() - add_arg = parser.add_argument - - add_arg("prompt") - add_arg("-s", "--steps", type=str, - help="number of steps") - add_arg("-S", "--seed", type=str, - help="image seed") - add_arg("-n", "--iterations", type=int, default=1, - help="number of samplings to perform (slower than batches, but will provide seeds for individual images)") - add_arg("-b", "--batch_size", type=int, default=1, - help="number of images to produce per sampling (will not provide seeds for individual images!)") - add_arg("-W", "--width", type=str, - help="image width, must be a multiple of 64") - add_arg("-H", "--height", type=str, - help="image height, must be a multiple of 64") - add_arg("-C", "--cfg_scale", type=str, default="7", - help="prompt configuration scale") - add_arg("-g", "--grid", action="store_true", - help="generate a grid") - add_arg("-i", "--individual", action="store_true", - help="generate individual files (default)") - add_arg("-I", "--init_img", type=str, - help="path to input image for img2img mode (supersedes width and height)") - add_arg("-f", "--strength", type=str, default="0.75", - help="strength for noising/unnoising. 0.0 preserves image exactly, 1.0 replaces it completely") - add_arg("-r", "--repeats", type=int, default=0, - help="number of times values are incremented") - add_arg("-B", "--feedback", action="store_true", - help="feeds the first generated image back into the next one as an init_img") - add_arg("-x", "--skip_normalize", action="store_true", - help="skip subprompt weight normalization") - - return parser - - -def change_dir(elements) -> str: - if len(elements) == 2: - d = elements[1] - if os.path.exists(d): - return d - print(f"Directory '{d} does not exist.'") - else: - print("Invalid number of arguments. Usage: cd ") - -if readline_available: - def init_readline() -> None: - readline.set_completer(Completer( - ["cd", "pwd", "help", "q"] - ).complete) - readline.set_completer_delims(" ") - readline.parse_and_bind("tab: complete") - load_history() - - def load_history() -> None: - histfile = os.path.join(os.path.expanduser("~"), ".morph_history") - - try: - readline.read_history_file(histfile) - readline.set_history_length(1000) - except FileNotFoundError: - pass - - atexit.register(readline.write_history_file, histfile) - - class Completer(): - def __init__(self, options: list): - self.options = options + list(parse_cmd()._option_string_actions.keys()) - - def complete(self, text, state): - buffer = readline.get_line_buffer() - - if text.startswith(("-I", "--init_img")): - return self._path_completions(text, state, (".png")) - - if buffer.strip().endswith("cd") or text.startswith((".", "/")): - return self._path_completions(text, state, ()) - - response = None - - if state == 0: - # This is the first time for this text, so build a match list. - if text: - self.matches = [ - s for s in self.options if s and s.startswith(text) - ] - else: - self.matches = self.options[:] - - # Return state'th item from the match list, if we have that many. - try: - response = self.matches[state] - except IndexError: - response = None - - return response - - def _path_completions(self, text, state, extensions): - # get the path so far - if text.startswith("-I"): - path = text.replace("-I", "", 1).lstrip() - elif text.startswith("--init_img="): - path = text.replace("--init_img=", "", 1).lstrip() - else: - path = text - - matches = [] - path = os.path.expanduser(path) - - if not len(path): - matches.append(text + "./") - else: - directory = os.path.dirname(path) - dir_list = os.listdri(directory) - - for n in dir_list: - if n.startswith(".") and len(n): - continue - - full_path = os.path.join(directory, n) - - if full_path.startswith(path): - if os.path.isdir(full_path): - matches.append(os.path.join(os.path.dirname(text), n) + "/") - elif n.endswith(extensions): - matches.append(os.path.join(os.path.dirname(text), n)) - - try: - response = matches[state] - except IndexError: - response = None - - return response - - -if __name__ == "__main__": - init() - From aec0dc006e08787ebb14121f693b6741d3c5cc0c Mon Sep 17 00:00:00 2001 From: yun saki Date: Thu, 25 Aug 2022 14:11:29 +0200 Subject: [PATCH 12/14] readd shebang --- scripts/dream.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/dream.py b/scripts/dream.py index e379b9f750e..4678792e9e9 100644 --- a/scripts/dream.py +++ b/scripts/dream.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 # Copyright (c) 2022 Lincoln D. Stein (https://github.com/lstein) import argparse From 1720286ef6bfe5bd83f66e6cede994d664bb9000 Mon Sep 17 00:00:00 2001 From: yun saki Date: Thu, 25 Aug 2022 19:00:00 +0200 Subject: [PATCH 13/14] fixed cd not terminating the current user_loop iteration --- scripts/dream.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/dream.py b/scripts/dream.py index 4678792e9e9..dc40c8ca450 100644 --- a/scripts/dream.py +++ b/scripts/dream.py @@ -160,12 +160,12 @@ def user_loop( if elements[0] == "cd": new_dir = change_dir(elements) - # skip if new_dir is empty - if not new_dir: - return False + # only set dir if not empty + if new_dir: + t2i.outdir = new_dir - t2i.outdir = new_dir del new_dir + return False # print output directory if elements[0] == "pwd": From e0fb460db522d3f6fb3116f601ac1879cabd504b Mon Sep 17 00:00:00 2001 From: yun saki Date: Thu, 25 Aug 2022 19:16:08 +0200 Subject: [PATCH 14/14] fixed variable name error --- scripts/dream.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/dream.py b/scripts/dream.py index dc40c8ca450..5a99ffe4a5b 100644 --- a/scripts/dream.py +++ b/scripts/dream.py @@ -230,7 +230,7 @@ def generate(cmd_opts: argparse.Namespace) -> None: if not cmd_opts.init_img: results.append([img + [t2i_args] for img in t2i.txt2img(**t2i_args)]) else: - assert os.path.exists(opt.init_img), f"No file found at {cmd_opts.init_img}. On Linux systems, pressing after -I will autocomplete a list of possible image files." + assert os.path.exists(cmd_opts.init_img), f"No file found at {cmd_opts.init_img}. On Linux systems, pressing after -I will autocomplete a list of possible image files." if cmd_opts.width or cmd_opts.height: print("Warning: width and height options are ignored when modifying an init image")