Merge branch 'development' into postprocessing-commands
@ -5,8 +5,7 @@ SAMPLES_DIR=${OUT_DIR}
|
||||
python scripts/dream.py \
|
||||
--from_file ${PROMPT_FILE} \
|
||||
--outdir ${OUT_DIR} \
|
||||
--sampler plms \
|
||||
--full_precision
|
||||
--sampler plms
|
||||
|
||||
# original output by CompVis/stable-diffusion
|
||||
IMAGE1=".dev_scripts/images/v1_4_astronaut_rides_horse_plms_step50_seed42.png"
|
||||
|
4
.github/workflows/mkdocs-flow.yml
vendored
@ -12,7 +12,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Build
|
||||
uses: Tiryoh/actions-mkdocs@v0
|
||||
with:
|
||||
|
4
.github/workflows/test-dream-conda.yml
vendored
@ -85,9 +85,9 @@ jobs:
|
||||
fi
|
||||
# Utterly hacky, but I don't know how else to do this
|
||||
if [[ ${{ github.ref }} == 'refs/heads/master' ]]; then
|
||||
time ${{ steps.vars.outputs.PYTHON_BIN }} scripts/dream.py --from_file tests/preflight_prompts.txt --full_precision
|
||||
time ${{ steps.vars.outputs.PYTHON_BIN }} scripts/dream.py --from_file tests/preflight_prompts.txt
|
||||
elif [[ ${{ github.ref }} == 'refs/heads/development' ]]; then
|
||||
time ${{ steps.vars.outputs.PYTHON_BIN }} scripts/dream.py --from_file tests/dev_prompts.txt --full_precision
|
||||
time ${{ steps.vars.outputs.PYTHON_BIN }} scripts/dream.py --from_file tests/dev_prompts.txt
|
||||
fi
|
||||
mkdir -p outputs/img-samples
|
||||
- name: Archive results
|
||||
|
3
.gitignore
vendored
@ -3,6 +3,9 @@ outputs/
|
||||
models/ldm/stable-diffusion-v1/model.ckpt
|
||||
ldm/restoration/codeformer/weights
|
||||
|
||||
# ignore the Anaconda/Miniconda installer used while building Docker image
|
||||
anaconda.sh
|
||||
|
||||
# ignore a directory which serves as a place for initial images
|
||||
inputs/
|
||||
|
||||
|
@ -5,9 +5,9 @@ singleQuote: true
|
||||
quoteProps: as-needed
|
||||
embeddedLanguageFormatting: auto
|
||||
overrides:
|
||||
- files: "*.md"
|
||||
- files: '*.md'
|
||||
options:
|
||||
proseWrap: always
|
||||
printWidth: 100
|
||||
printWidth: 80
|
||||
parser: markdown
|
||||
cursorOffset: -1
|
||||
|
22
README.md
@ -1,4 +1,4 @@
|
||||
<div align="center">
|
||||
<h1 align='center'><b>InvokeAI: A Stable Diffusion Toolkit</b></h1>
|
||||
|
||||
# Stable Diffusion Dream Script
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<a href="https://discord.gg/ZmtBAhwWhy"><img src="docs/assets/join-us-on-discord-image.png"/></a>
|
||||
</p>
|
||||
|
||||
# **Stable Diffusion Dream Script**
|
||||
# **InvokeAI - A Stable Diffusion Toolkit**
|
||||
[![discord badge]][discord link]
|
||||
|
||||
[![latest release badge]][latest release link] [![github stars badge]][github stars link] [![github forks badge]][github forks link]
|
||||
@ -86,17 +86,14 @@ You wil need one of the following:
|
||||
|
||||
- At least 6 GB of free disk space for the machine learning model, Python, and all its dependencies.
|
||||
|
||||
> Note
|
||||
>
|
||||
> If you have an Nvidia 10xx series card (e.g. the 1080ti), please run the dream script in
|
||||
> full-precision mode as shown below.
|
||||
#### Note
|
||||
|
||||
Similarly, specify full-precision mode on Apple M1 hardware.
|
||||
|
||||
To run in full-precision mode, start `dream.py` with the `--full_precision` flag:
|
||||
Precision is auto configured based on the device. If however you encounter
|
||||
errors like 'expected type Float but found Half' or 'not implemented for Half'
|
||||
you can try starting `dream.py` with the `--precision=float32` flag:
|
||||
|
||||
```bash
|
||||
(ldm) ~/stable-diffusion$ python scripts/dream.py --full_precision
|
||||
(ldm) ~/stable-diffusion$ python scripts/dream.py --precision=float32
|
||||
```
|
||||
|
||||
### Features
|
||||
@ -125,6 +122,11 @@ To run in full-precision mode, start `dream.py` with the `--full_precision` flag
|
||||
|
||||
### Latest Changes
|
||||
|
||||
- vNEXT (TODO 2022)
|
||||
|
||||
- Deprecated `--full_precision` / `-F`. Simply omit it and `dream.py` will auto
|
||||
configure. To switch away from auto use the new flag like `--precision=float32`.
|
||||
|
||||
- v1.14 (11 September 2022)
|
||||
|
||||
- Memory optimizations for small-RAM cards. 512x512 now possible on 4 GB GPUs.
|
||||
|
@ -2,14 +2,14 @@ from modules.parse_seed_weights import parse_seed_weights
|
||||
import argparse
|
||||
|
||||
SAMPLER_CHOICES = [
|
||||
'ddim',
|
||||
'k_dpm_2_a',
|
||||
'k_dpm_2',
|
||||
'k_euler_a',
|
||||
'k_euler',
|
||||
'k_heun',
|
||||
'k_lms',
|
||||
'plms',
|
||||
"ddim",
|
||||
"k_dpm_2_a",
|
||||
"k_dpm_2",
|
||||
"k_euler_a",
|
||||
"k_euler",
|
||||
"k_heun",
|
||||
"k_lms",
|
||||
"plms",
|
||||
]
|
||||
|
||||
|
||||
@ -20,194 +20,42 @@ def parameters_to_command(params):
|
||||
|
||||
switches = list()
|
||||
|
||||
if 'prompt' in params:
|
||||
if "prompt" in params:
|
||||
switches.append(f'"{params["prompt"]}"')
|
||||
if 'steps' in params:
|
||||
if "steps" in params:
|
||||
switches.append(f'-s {params["steps"]}')
|
||||
if 'seed' in params:
|
||||
if "seed" in params:
|
||||
switches.append(f'-S {params["seed"]}')
|
||||
if 'width' in params:
|
||||
if "width" in params:
|
||||
switches.append(f'-W {params["width"]}')
|
||||
if 'height' in params:
|
||||
if "height" in params:
|
||||
switches.append(f'-H {params["height"]}')
|
||||
if 'cfg_scale' in params:
|
||||
if "cfg_scale" in params:
|
||||
switches.append(f'-C {params["cfg_scale"]}')
|
||||
if 'sampler_name' in params:
|
||||
if "sampler_name" in params:
|
||||
switches.append(f'-A {params["sampler_name"]}')
|
||||
if 'seamless' in params and params["seamless"] == True:
|
||||
switches.append(f'--seamless')
|
||||
if 'init_img' in params and len(params['init_img']) > 0:
|
||||
if "seamless" in params and params["seamless"] == True:
|
||||
switches.append(f"--seamless")
|
||||
if "init_img" in params and len(params["init_img"]) > 0:
|
||||
switches.append(f'-I {params["init_img"]}')
|
||||
if 'init_mask' in params and len(params['init_mask']) > 0:
|
||||
if "init_mask" in params and len(params["init_mask"]) > 0:
|
||||
switches.append(f'-M {params["init_mask"]}')
|
||||
if 'init_color' in params and len(params['init_color']) > 0:
|
||||
if "init_color" in params and len(params["init_color"]) > 0:
|
||||
switches.append(f'--init_color {params["init_color"]}')
|
||||
if 'strength' in params and 'init_img' in params:
|
||||
if "strength" in params and "init_img" in params:
|
||||
switches.append(f'-f {params["strength"]}')
|
||||
if 'fit' in params and params["fit"] == True:
|
||||
switches.append(f'--fit')
|
||||
if 'gfpgan_strength' in params and params["gfpgan_strength"]:
|
||||
if "fit" in params and params["fit"] == True:
|
||||
switches.append(f"--fit")
|
||||
if "gfpgan_strength" in params and params["gfpgan_strength"]:
|
||||
switches.append(f'-G {params["gfpgan_strength"]}')
|
||||
if 'upscale' in params and params["upscale"]:
|
||||
if "upscale" in params and params["upscale"]:
|
||||
switches.append(f'-U {params["upscale"][0]} {params["upscale"][1]}')
|
||||
if 'variation_amount' in params and params['variation_amount'] > 0:
|
||||
if "variation_amount" in params and params["variation_amount"] > 0:
|
||||
switches.append(f'-v {params["variation_amount"]}')
|
||||
if 'with_variations' in params:
|
||||
seed_weight_pairs = ','.join(f'{seed}:{weight}' for seed, weight in params["with_variations"])
|
||||
switches.append(f'-V {seed_weight_pairs}')
|
||||
if "with_variations" in params:
|
||||
seed_weight_pairs = ",".join(
|
||||
f"{seed}:{weight}" for seed, weight in params["with_variations"]
|
||||
)
|
||||
switches.append(f"-V {seed_weight_pairs}")
|
||||
|
||||
return ' '.join(switches)
|
||||
|
||||
|
||||
|
||||
def create_cmd_parser():
|
||||
"""
|
||||
This is simply a copy of the parser from `dream.py` with a change to give
|
||||
prompt a default value. This is a temporary hack pending merge of #587 which
|
||||
provides a better way to do this.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Example: dream> a fantastic alien landscape -W1024 -H960 -s100 -n12',
|
||||
exit_on_error=True,
|
||||
)
|
||||
parser.add_argument('prompt', nargs='?', default='')
|
||||
parser.add_argument('-s', '--steps', type=int, help='Number of steps')
|
||||
parser.add_argument(
|
||||
'-S',
|
||||
'--seed',
|
||||
type=int,
|
||||
help='Image seed; a +ve integer, or use -1 for the previous seed, -2 for the one before that, etc',
|
||||
)
|
||||
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(
|
||||
'-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='Classifier free guidance (CFG) scale - higher numbers cause generator to "try" harder.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-g', '--grid', action='store_true', help='generate a grid'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--outdir',
|
||||
'-o',
|
||||
type=str,
|
||||
default=None,
|
||||
help='Directory to save generated images and a log of prompts and seeds',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--seamless',
|
||||
action='store_true',
|
||||
help='Change the model to seamless tiling (circular) mode',
|
||||
)
|
||||
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(
|
||||
'-M',
|
||||
'--init_mask',
|
||||
type=str,
|
||||
help='Path to input mask for inpainting mode (supersedes width and height)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--init_color',
|
||||
type=str,
|
||||
help='Path to reference image for color correction (used for repeated img2img and inpainting)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-T',
|
||||
'-fit',
|
||||
'--fit',
|
||||
action='store_true',
|
||||
help='If specified, will resize the input image to fit within the dimensions of width x height (512x512 default)',
|
||||
)
|
||||
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(
|
||||
'-G',
|
||||
'--gfpgan_strength',
|
||||
default=0,
|
||||
type=float,
|
||||
help='The strength at which to apply the GFPGAN model to the result, in order to improve faces.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-U',
|
||||
'--upscale',
|
||||
nargs='+',
|
||||
default=None,
|
||||
type=float,
|
||||
help='Scale factor (2, 4) for upscaling followed by upscaling strength (0-1.0). If strength not specified, defaults to 0.75'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-save_orig',
|
||||
'--save_original',
|
||||
action='store_true',
|
||||
help='Save original. Use it when upscaling to save both versions.',
|
||||
)
|
||||
# variants is going to be superseded by a generalized "prompt-morph" function
|
||||
# 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',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-A',
|
||||
'-m',
|
||||
'--sampler',
|
||||
dest='sampler_name',
|
||||
default=None,
|
||||
type=str,
|
||||
choices=SAMPLER_CHOICES,
|
||||
metavar='SAMPLER_NAME',
|
||||
help=f'Switch to a different sampler. Supported samplers: {", ".join(SAMPLER_CHOICES)}',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-t',
|
||||
'--log_tokenization',
|
||||
action='store_true',
|
||||
help='shows how the prompt is split into tokens'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-v',
|
||||
'--variation_amount',
|
||||
default=0.0,
|
||||
type=float,
|
||||
help='If > 0, generates variations on the initial seed instead of random seeds per iteration. Must be between 0 and 1. Higher values will be more different.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-V',
|
||||
'--with_variations',
|
||||
default=None,
|
||||
type=str,
|
||||
help='list of variations to apply, in the format `seed:weight,seed:weight,...'
|
||||
)
|
||||
return parser
|
||||
return " ".join(switches)
|
||||
|
@ -6,7 +6,8 @@ import traceback
|
||||
import eventlet
|
||||
import glob
|
||||
import shlex
|
||||
import argparse
|
||||
import math
|
||||
import shutil
|
||||
|
||||
from flask_socketio import SocketIO
|
||||
from flask import Flask, send_from_directory, url_for, jsonify
|
||||
@ -15,13 +16,16 @@ from PIL import Image
|
||||
from pytorch_lightning import logging
|
||||
from threading import Event
|
||||
from uuid import uuid4
|
||||
from send2trash import send2trash
|
||||
|
||||
from ldm.gfpgan.gfpgan_tools import real_esrgan_upscale
|
||||
from ldm.gfpgan.gfpgan_tools import run_gfpgan
|
||||
from ldm.generate import Generate
|
||||
from ldm.dream.pngwriter import PngWriter, retrieve_metadata
|
||||
from ldm.dream.args import APP_ID, APP_VERSION, calculate_init_img_hash
|
||||
from ldm.dream.conditioning import split_weighted_subprompts
|
||||
|
||||
from modules.parameters import parameters_to_command, create_cmd_parser
|
||||
from modules.parameters import parameters_to_command
|
||||
|
||||
|
||||
"""
|
||||
@ -29,12 +33,14 @@ USER CONFIG
|
||||
"""
|
||||
|
||||
output_dir = "outputs/" # Base output directory for images
|
||||
#host = 'localhost' # Web & socket.io host
|
||||
host = '0.0.0.0' # Web & socket.io host
|
||||
# host = 'localhost' # Web & socket.io host
|
||||
host = "localhost" # Web & socket.io host
|
||||
port = 9090 # Web & socket.io port
|
||||
verbose = False # enables copious socket.io logging
|
||||
additional_allowed_origins = ['http://localhost:9090'] # additional CORS allowed origins
|
||||
|
||||
verbose = False # enables copious socket.io logging
|
||||
additional_allowed_origins = [
|
||||
"http://localhost:5173"
|
||||
] # additional CORS allowed origins
|
||||
model = "stable-diffusion-1.4"
|
||||
|
||||
"""
|
||||
END USER CONFIG
|
||||
@ -47,26 +53,23 @@ SERVER SETUP
|
||||
|
||||
|
||||
# fix missing mimetypes on windows due to registry wonkiness
|
||||
mimetypes.add_type('application/javascript', '.js')
|
||||
mimetypes.add_type('text/css', '.css')
|
||||
mimetypes.add_type("application/javascript", ".js")
|
||||
mimetypes.add_type("text/css", ".css")
|
||||
|
||||
app = Flask(__name__, static_url_path='', static_folder='../frontend/dist/')
|
||||
app = Flask(__name__, static_url_path="", static_folder="../frontend/dist/")
|
||||
|
||||
|
||||
app.config['OUTPUTS_FOLDER'] = "../outputs"
|
||||
app.config["OUTPUTS_FOLDER"] = "../outputs"
|
||||
|
||||
|
||||
@app.route('/outputs/<path:filename>')
|
||||
@app.route("/outputs/<path:filename>")
|
||||
def outputs(filename):
|
||||
return send_from_directory(
|
||||
app.config['OUTPUTS_FOLDER'],
|
||||
filename
|
||||
)
|
||||
return send_from_directory(app.config["OUTPUTS_FOLDER"], filename)
|
||||
|
||||
|
||||
@app.route("/", defaults={'path': ''})
|
||||
@app.route("/", defaults={"path": ""})
|
||||
def serve(path):
|
||||
return send_from_directory(app.static_folder, 'index.html')
|
||||
return send_from_directory(app.static_folder, "index.html")
|
||||
|
||||
|
||||
logger = True if verbose else False
|
||||
@ -78,12 +81,12 @@ max_http_buffer_size = 10000000
|
||||
cors_allowed_origins = [f"http://{host}:{port}"] + additional_allowed_origins
|
||||
|
||||
socketio = SocketIO(
|
||||
app,
|
||||
logger=logger,
|
||||
engineio_logger=engineio_logger,
|
||||
max_http_buffer_size=max_http_buffer_size,
|
||||
cors_allowed_origins=cors_allowed_origins,
|
||||
)
|
||||
app,
|
||||
logger=logger,
|
||||
engineio_logger=engineio_logger,
|
||||
max_http_buffer_size=max_http_buffer_size,
|
||||
cors_allowed_origins=cors_allowed_origins,
|
||||
)
|
||||
|
||||
|
||||
"""
|
||||
@ -104,29 +107,31 @@ canceled = Event()
|
||||
|
||||
# reduce logging outputs to error
|
||||
transformers.logging.set_verbosity_error()
|
||||
logging.getLogger('pytorch_lightning').setLevel(logging.ERROR)
|
||||
logging.getLogger("pytorch_lightning").setLevel(logging.ERROR)
|
||||
|
||||
# Initialize and load model
|
||||
model = Generate()
|
||||
model.load_model()
|
||||
generate = Generate(model)
|
||||
generate.load_model()
|
||||
|
||||
|
||||
# location for "finished" images
|
||||
result_path = os.path.join(output_dir, 'img-samples/')
|
||||
result_path = os.path.join(output_dir, "img-samples/")
|
||||
|
||||
# temporary path for intermediates
|
||||
intermediate_path = os.path.join(result_path, 'intermediates/')
|
||||
intermediate_path = os.path.join(result_path, "intermediates/")
|
||||
|
||||
# path for user-uploaded init images and masks
|
||||
init_path = os.path.join(result_path, 'init-images/')
|
||||
mask_path = os.path.join(result_path, 'mask-images/')
|
||||
init_image_path = os.path.join(result_path, "init-images/")
|
||||
mask_image_path = os.path.join(result_path, "mask-images/")
|
||||
|
||||
# txt log
|
||||
log_path = os.path.join(result_path, 'dream_log.txt')
|
||||
log_path = os.path.join(result_path, "dream_log.txt")
|
||||
|
||||
# make all output paths
|
||||
[os.makedirs(path, exist_ok=True)
|
||||
for path in [result_path, intermediate_path, init_path, mask_path]]
|
||||
[
|
||||
os.makedirs(path, exist_ok=True)
|
||||
for path in [result_path, intermediate_path, init_image_path, mask_image_path]
|
||||
]
|
||||
|
||||
|
||||
"""
|
||||
@ -139,126 +144,219 @@ SOCKET.IO LISTENERS
|
||||
"""
|
||||
|
||||
|
||||
@socketio.on('requestAllImages')
|
||||
@socketio.on("requestSystemConfig")
|
||||
def handle_request_capabilities():
|
||||
print(f">> System config requested")
|
||||
config = get_system_config()
|
||||
socketio.emit("systemConfig", config)
|
||||
|
||||
|
||||
@socketio.on("requestAllImages")
|
||||
def handle_request_all_images():
|
||||
print(f'>> All images requested')
|
||||
parser = create_cmd_parser()
|
||||
print(f">> All images requested")
|
||||
paths = list(filter(os.path.isfile, glob.glob(result_path + "*.png")))
|
||||
paths.sort(key=lambda x: os.path.getmtime(x))
|
||||
image_array = []
|
||||
for path in paths:
|
||||
# image = Image.open(path)
|
||||
all_metadata = retrieve_metadata(path)
|
||||
if 'Dream' in all_metadata and not all_metadata['sd-metadata']:
|
||||
metadata = vars(parser.parse_args(shlex.split(all_metadata['Dream'])))
|
||||
else:
|
||||
metadata = all_metadata['sd-metadata']
|
||||
image_array.append({'path': path, 'metadata': metadata})
|
||||
return make_response("OK", data=image_array)
|
||||
metadata = retrieve_metadata(path)
|
||||
image_array.append({"url": path, "metadata": metadata["sd-metadata"]})
|
||||
socketio.emit("galleryImages", {"images": image_array})
|
||||
eventlet.sleep(0)
|
||||
|
||||
|
||||
@socketio.on('generateImage')
|
||||
def handle_generate_image_event(generation_parameters, esrgan_parameters, gfpgan_parameters):
|
||||
print(f'>> Image generation requested: {generation_parameters}\nESRGAN parameters: {esrgan_parameters}\nGFPGAN parameters: {gfpgan_parameters}')
|
||||
generate_images(
|
||||
generation_parameters,
|
||||
esrgan_parameters,
|
||||
gfpgan_parameters
|
||||
@socketio.on("generateImage")
|
||||
def handle_generate_image_event(
|
||||
generation_parameters, esrgan_parameters, gfpgan_parameters
|
||||
):
|
||||
print(
|
||||
f">> Image generation requested: {generation_parameters}\nESRGAN parameters: {esrgan_parameters}\nGFPGAN parameters: {gfpgan_parameters}"
|
||||
)
|
||||
return make_response("OK")
|
||||
generate_images(generation_parameters, esrgan_parameters, gfpgan_parameters)
|
||||
|
||||
|
||||
@socketio.on('runESRGAN')
|
||||
@socketio.on("runESRGAN")
|
||||
def handle_run_esrgan_event(original_image, esrgan_parameters):
|
||||
print(f'>> ESRGAN upscale requested for "{original_image["url"]}": {esrgan_parameters}')
|
||||
print(
|
||||
f'>> ESRGAN upscale requested for "{original_image["url"]}": {esrgan_parameters}'
|
||||
)
|
||||
progress = {
|
||||
"currentStep": 1,
|
||||
"totalSteps": 1,
|
||||
"currentIteration": 1,
|
||||
"totalIterations": 1,
|
||||
"currentStatus": "Preparing",
|
||||
"isProcessing": True,
|
||||
"currentStatusHasSteps": False,
|
||||
}
|
||||
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
image = Image.open(original_image["url"])
|
||||
|
||||
seed = original_image['metadata']['seed'] if 'seed' in original_image['metadata'] else 'unknown_seed'
|
||||
seed = (
|
||||
original_image["metadata"]["seed"]
|
||||
if "seed" in original_image["metadata"]
|
||||
else "unknown_seed"
|
||||
)
|
||||
|
||||
progress["currentStatus"] = "Upscaling"
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
image = real_esrgan_upscale(
|
||||
image=image,
|
||||
upsampler_scale=esrgan_parameters['upscale'][0],
|
||||
strength=esrgan_parameters['upscale'][1],
|
||||
seed=seed
|
||||
upsampler_scale=esrgan_parameters["upscale"][0],
|
||||
strength=esrgan_parameters["upscale"][1],
|
||||
seed=seed,
|
||||
)
|
||||
|
||||
esrgan_parameters['seed'] = seed
|
||||
path = save_image(image, esrgan_parameters, result_path, postprocessing='esrgan')
|
||||
progress["currentStatus"] = "Saving image"
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
esrgan_parameters["seed"] = seed
|
||||
metadata = parameters_to_post_processed_image_metadata(
|
||||
parameters=esrgan_parameters,
|
||||
original_image_path=original_image["url"],
|
||||
type="esrgan",
|
||||
)
|
||||
command = parameters_to_command(esrgan_parameters)
|
||||
|
||||
path = save_image(image, command, metadata, result_path, postprocessing="esrgan")
|
||||
|
||||
write_log_message(f'[Upscaled] "{original_image["url"]}" > "{path}": {command}')
|
||||
|
||||
progress["currentStatus"] = "Finished"
|
||||
progress["currentStep"] = 0
|
||||
progress["totalSteps"] = 0
|
||||
progress["currentIteration"] = 0
|
||||
progress["totalIterations"] = 0
|
||||
progress["isProcessing"] = False
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
socketio.emit(
|
||||
'result', {'url': os.path.relpath(path), 'type': 'esrgan', 'uuid': original_image['uuid'],'metadata': esrgan_parameters})
|
||||
"esrganResult",
|
||||
{
|
||||
"url": os.path.relpath(path),
|
||||
"metadata": metadata,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@socketio.on('runGFPGAN')
|
||||
@socketio.on("runGFPGAN")
|
||||
def handle_run_gfpgan_event(original_image, gfpgan_parameters):
|
||||
print(f'>> GFPGAN face fix requested for "{original_image["url"]}": {gfpgan_parameters}')
|
||||
print(
|
||||
f'>> GFPGAN face fix requested for "{original_image["url"]}": {gfpgan_parameters}'
|
||||
)
|
||||
progress = {
|
||||
"currentStep": 1,
|
||||
"totalSteps": 1,
|
||||
"currentIteration": 1,
|
||||
"totalIterations": 1,
|
||||
"currentStatus": "Preparing",
|
||||
"isProcessing": True,
|
||||
"currentStatusHasSteps": False,
|
||||
}
|
||||
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
image = Image.open(original_image["url"])
|
||||
|
||||
seed = original_image['metadata']['seed'] if 'seed' in original_image['metadata'] else 'unknown_seed'
|
||||
seed = (
|
||||
original_image["metadata"]["seed"]
|
||||
if "seed" in original_image["metadata"]
|
||||
else "unknown_seed"
|
||||
)
|
||||
|
||||
progress["currentStatus"] = "Fixing faces"
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
image = run_gfpgan(
|
||||
image=image,
|
||||
strength=gfpgan_parameters['gfpgan_strength'],
|
||||
strength=gfpgan_parameters["gfpgan_strength"],
|
||||
seed=seed,
|
||||
upsampler_scale=1
|
||||
upsampler_scale=1,
|
||||
)
|
||||
|
||||
gfpgan_parameters['seed'] = seed
|
||||
path = save_image(image, gfpgan_parameters, result_path, postprocessing='gfpgan')
|
||||
progress["currentStatus"] = "Saving image"
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
gfpgan_parameters["seed"] = seed
|
||||
metadata = parameters_to_post_processed_image_metadata(
|
||||
parameters=gfpgan_parameters,
|
||||
original_image_path=original_image["url"],
|
||||
type="gfpgan",
|
||||
)
|
||||
command = parameters_to_command(gfpgan_parameters)
|
||||
|
||||
path = save_image(image, command, metadata, result_path, postprocessing="gfpgan")
|
||||
|
||||
write_log_message(f'[Fixed faces] "{original_image["url"]}" > "{path}": {command}')
|
||||
|
||||
progress["currentStatus"] = "Finished"
|
||||
progress["currentStep"] = 0
|
||||
progress["totalSteps"] = 0
|
||||
progress["currentIteration"] = 0
|
||||
progress["totalIterations"] = 0
|
||||
progress["isProcessing"] = False
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
socketio.emit(
|
||||
'result', {'url': os.path.relpath(path), 'type': 'gfpgan', 'uuid': original_image['uuid'],'metadata': gfpgan_parameters})
|
||||
"gfpganResult",
|
||||
{
|
||||
"url": os.path.relpath(path),
|
||||
"metadata": metadata,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@socketio.on('cancel')
|
||||
@socketio.on("cancel")
|
||||
def handle_cancel():
|
||||
print(f'>> Cancel processing requested')
|
||||
print(f">> Cancel processing requested")
|
||||
canceled.set()
|
||||
return make_response("OK")
|
||||
socketio.emit("processingCanceled")
|
||||
|
||||
|
||||
# TODO: I think this needs a safety mechanism.
|
||||
@socketio.on('deleteImage')
|
||||
def handle_delete_image(path):
|
||||
@socketio.on("deleteImage")
|
||||
def handle_delete_image(path, uuid):
|
||||
print(f'>> Delete requested "{path}"')
|
||||
Path(path).unlink()
|
||||
return make_response("OK")
|
||||
send2trash(path)
|
||||
socketio.emit("imageDeleted", {"url": path, "uuid": uuid})
|
||||
|
||||
|
||||
# TODO: I think this needs a safety mechanism.
|
||||
@socketio.on('uploadInitialImage')
|
||||
@socketio.on("uploadInitialImage")
|
||||
def handle_upload_initial_image(bytes, name):
|
||||
print(f'>> Init image upload requested "{name}"')
|
||||
uuid = uuid4().hex
|
||||
split = os.path.splitext(name)
|
||||
name = f'{split[0]}.{uuid}{split[1]}'
|
||||
file_path = os.path.join(init_path, name)
|
||||
name = f"{split[0]}.{uuid}{split[1]}"
|
||||
file_path = os.path.join(init_image_path, name)
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
newFile = open(file_path, "wb")
|
||||
newFile.write(bytes)
|
||||
return make_response("OK", data=file_path)
|
||||
socketio.emit("initialImageUploaded", {"url": file_path, "uuid": ""})
|
||||
|
||||
|
||||
# TODO: I think this needs a safety mechanism.
|
||||
@socketio.on('uploadMaskImage')
|
||||
@socketio.on("uploadMaskImage")
|
||||
def handle_upload_mask_image(bytes, name):
|
||||
print(f'>> Mask image upload requested "{name}"')
|
||||
uuid = uuid4().hex
|
||||
split = os.path.splitext(name)
|
||||
name = f'{split[0]}.{uuid}{split[1]}'
|
||||
file_path = os.path.join(mask_path, name)
|
||||
name = f"{split[0]}.{uuid}{split[1]}"
|
||||
file_path = os.path.join(mask_image_path, name)
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
newFile = open(file_path, "wb")
|
||||
newFile.write(bytes)
|
||||
return make_response("OK", data=file_path)
|
||||
|
||||
socketio.emit("maskImageUploaded", {"url": file_path, "uuid": ""})
|
||||
|
||||
|
||||
"""
|
||||
@ -266,114 +364,343 @@ END SOCKET.IO LISTENERS
|
||||
"""
|
||||
|
||||
|
||||
|
||||
"""
|
||||
ADDITIONAL FUNCTIONS
|
||||
"""
|
||||
|
||||
|
||||
def get_system_config():
|
||||
return {
|
||||
"model": "stable diffusion",
|
||||
"model_id": model,
|
||||
"model_hash": generate.model_hash,
|
||||
"app_id": APP_ID,
|
||||
"app_version": APP_VERSION,
|
||||
}
|
||||
|
||||
|
||||
def parameters_to_post_processed_image_metadata(parameters, original_image_path, type):
|
||||
# top-level metadata minus `image` or `images`
|
||||
metadata = get_system_config()
|
||||
|
||||
orig_hash = calculate_init_img_hash(original_image_path)
|
||||
|
||||
image = {"orig_path": original_image_path, "orig_hash": orig_hash}
|
||||
|
||||
if type == "esrgan":
|
||||
image["type"] = "esrgan"
|
||||
image["scale"] = parameters["upscale"][0]
|
||||
image["strength"] = parameters["upscale"][1]
|
||||
elif type == "gfpgan":
|
||||
image["type"] = "gfpgan"
|
||||
image["strength"] = parameters["gfpgan_strength"]
|
||||
else:
|
||||
raise TypeError(f"Invalid type: {type}")
|
||||
|
||||
metadata["image"] = image
|
||||
return metadata
|
||||
|
||||
|
||||
def parameters_to_generated_image_metadata(parameters):
|
||||
# top-level metadata minus `image` or `images`
|
||||
|
||||
metadata = get_system_config()
|
||||
# remove any image keys not mentioned in RFC #266
|
||||
rfc266_img_fields = [
|
||||
"type",
|
||||
"postprocessing",
|
||||
"sampler",
|
||||
"prompt",
|
||||
"seed",
|
||||
"variations",
|
||||
"steps",
|
||||
"cfg_scale",
|
||||
"step_number",
|
||||
"width",
|
||||
"height",
|
||||
"extra",
|
||||
"seamless",
|
||||
]
|
||||
|
||||
rfc_dict = {}
|
||||
|
||||
for item in parameters.items():
|
||||
key, value = item
|
||||
if key in rfc266_img_fields:
|
||||
rfc_dict[key] = value
|
||||
|
||||
postprocessing = []
|
||||
|
||||
# 'postprocessing' is either null or an
|
||||
if "gfpgan_strength" in parameters:
|
||||
|
||||
postprocessing.append(
|
||||
{"type": "gfpgan", "strength": float(parameters["gfpgan_strength"])}
|
||||
)
|
||||
|
||||
if "upscale" in parameters:
|
||||
postprocessing.append(
|
||||
{
|
||||
"type": "esrgan",
|
||||
"scale": int(parameters["upscale"][0]),
|
||||
"strength": float(parameters["upscale"][1]),
|
||||
}
|
||||
)
|
||||
|
||||
rfc_dict["postprocessing"] = postprocessing if len(postprocessing) > 0 else None
|
||||
|
||||
# semantic drift
|
||||
rfc_dict["sampler"] = parameters["sampler_name"]
|
||||
|
||||
# display weighted subprompts (liable to change)
|
||||
subprompts = split_weighted_subprompts(parameters["prompt"])
|
||||
subprompts = [{"prompt": x[0], "weight": x[1]} for x in subprompts]
|
||||
rfc_dict["prompt"] = subprompts
|
||||
|
||||
# 'variations' should always exist and be an array, empty or consisting of {'seed': seed, 'weight': weight} pairs
|
||||
variations = []
|
||||
|
||||
if "with_variations" in parameters:
|
||||
variations = [
|
||||
{"seed": x[0], "weight": x[1]} for x in parameters["with_variations"]
|
||||
]
|
||||
|
||||
rfc_dict["variations"] = variations
|
||||
|
||||
if "init_img" in parameters:
|
||||
rfc_dict["type"] = "img2img"
|
||||
rfc_dict["strength"] = parameters["strength"]
|
||||
rfc_dict["fit"] = parameters["fit"] # TODO: Noncompliant
|
||||
rfc_dict["orig_hash"] = calculate_init_img_hash(parameters["init_img"])
|
||||
rfc_dict["init_image_path"] = parameters["init_img"] # TODO: Noncompliant
|
||||
rfc_dict["sampler"] = "ddim" # TODO: FIX ME WHEN IMG2IMG SUPPORTS ALL SAMPLERS
|
||||
if "init_mask" in parameters:
|
||||
rfc_dict["mask_hash"] = calculate_init_img_hash(
|
||||
parameters["init_mask"]
|
||||
) # TODO: Noncompliant
|
||||
rfc_dict["mask_image_path"] = parameters["init_mask"] # TODO: Noncompliant
|
||||
else:
|
||||
rfc_dict["type"] = "txt2img"
|
||||
|
||||
metadata["image"] = rfc_dict
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
def make_unique_init_image_filename(name):
|
||||
uuid = uuid4().hex
|
||||
split = os.path.splitext(name)
|
||||
name = f"{split[0]}.{uuid}{split[1]}"
|
||||
return name
|
||||
|
||||
|
||||
def write_log_message(message, log_path=log_path):
|
||||
"""Logs the filename and parameters used to generate or process that image to log file"""
|
||||
message = f'{message}\n'
|
||||
with open(log_path, 'a', encoding='utf-8') as file:
|
||||
message = f"{message}\n"
|
||||
with open(log_path, "a", encoding="utf-8") as file:
|
||||
file.writelines(message)
|
||||
|
||||
|
||||
def make_response(status, message=None, data=None):
|
||||
response = {'status': status}
|
||||
if message is not None:
|
||||
response['message'] = message
|
||||
if data is not None:
|
||||
response['data'] = data
|
||||
return response
|
||||
|
||||
|
||||
def save_image(image, parameters, output_dir, step_index=None, postprocessing=False):
|
||||
seed = parameters['seed'] if 'seed' in parameters else 'unknown_seed'
|
||||
|
||||
def save_image(
|
||||
image, command, metadata, output_dir, step_index=None, postprocessing=False
|
||||
):
|
||||
pngwriter = PngWriter(output_dir)
|
||||
prefix = pngwriter.unique_prefix()
|
||||
|
||||
filename = f'{prefix}.{seed}'
|
||||
seed = "unknown_seed"
|
||||
|
||||
if "image" in metadata:
|
||||
if "seed" in metadata["image"]:
|
||||
seed = metadata["image"]["seed"]
|
||||
|
||||
filename = f"{prefix}.{seed}"
|
||||
|
||||
if step_index:
|
||||
filename += f'.{step_index}'
|
||||
filename += f".{step_index}"
|
||||
if postprocessing:
|
||||
filename += f'.postprocessed'
|
||||
filename += f".postprocessed"
|
||||
|
||||
filename += '.png'
|
||||
filename += ".png"
|
||||
|
||||
command = parameters_to_command(parameters)
|
||||
|
||||
path = pngwriter.save_image_and_prompt_to_png(image, command, metadata=parameters, name=filename)
|
||||
path = pngwriter.save_image_and_prompt_to_png(
|
||||
image=image, dream_prompt=command, metadata=metadata, name=filename
|
||||
)
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def calculate_real_steps(steps, strength, has_init_image):
|
||||
return math.floor(strength * steps) if has_init_image else steps
|
||||
|
||||
|
||||
def generate_images(generation_parameters, esrgan_parameters, gfpgan_parameters):
|
||||
canceled.clear()
|
||||
|
||||
step_index = 1
|
||||
|
||||
"""
|
||||
If a result image is used as an init image, and then deleted, we will want to be
|
||||
able to use it as an init image in the future. Need to copy it.
|
||||
|
||||
If the init/mask image doesn't exist in the init_image_path/mask_image_path,
|
||||
make a unique filename for it and copy it there.
|
||||
"""
|
||||
if "init_img" in generation_parameters:
|
||||
filename = os.path.basename(generation_parameters["init_img"])
|
||||
if not os.path.exists(os.path.join(init_image_path, filename)):
|
||||
unique_filename = make_unique_init_image_filename(filename)
|
||||
new_path = os.path.join(init_image_path, unique_filename)
|
||||
shutil.copy(generation_parameters["init_img"], new_path)
|
||||
generation_parameters["init_img"] = new_path
|
||||
if "init_mask" in generation_parameters:
|
||||
filename = os.path.basename(generation_parameters["init_mask"])
|
||||
if not os.path.exists(os.path.join(mask_image_path, filename)):
|
||||
unique_filename = make_unique_init_image_filename(filename)
|
||||
new_path = os.path.join(init_image_path, unique_filename)
|
||||
shutil.copy(generation_parameters["init_img"], new_path)
|
||||
generation_parameters["init_mask"] = new_path
|
||||
|
||||
totalSteps = calculate_real_steps(
|
||||
steps=generation_parameters["steps"],
|
||||
strength=generation_parameters["strength"]
|
||||
if "strength" in generation_parameters
|
||||
else None,
|
||||
has_init_image="init_img" in generation_parameters,
|
||||
)
|
||||
|
||||
progress = {
|
||||
"currentStep": 1,
|
||||
"totalSteps": totalSteps,
|
||||
"currentIteration": 1,
|
||||
"totalIterations": generation_parameters["iterations"],
|
||||
"currentStatus": "Preparing",
|
||||
"isProcessing": True,
|
||||
"currentStatusHasSteps": False,
|
||||
}
|
||||
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
def image_progress(sample, step):
|
||||
if canceled.is_set():
|
||||
raise CanceledException
|
||||
|
||||
nonlocal step_index
|
||||
nonlocal generation_parameters
|
||||
if generation_parameters["progress_images"] and step % 5 == 0 and step < generation_parameters['steps'] - 1:
|
||||
image = model.sample_to_image(sample)
|
||||
path = save_image(image, generation_parameters, intermediate_path, step_index)
|
||||
nonlocal progress
|
||||
|
||||
progress["currentStep"] = step + 1
|
||||
progress["currentStatus"] = "Generating"
|
||||
progress["currentStatusHasSteps"] = True
|
||||
|
||||
if (
|
||||
generation_parameters["progress_images"]
|
||||
and step % 5 == 0
|
||||
and step < generation_parameters["steps"] - 1
|
||||
):
|
||||
image = generate.sample_to_image(sample)
|
||||
path = save_image(
|
||||
image, generation_parameters, intermediate_path, step_index
|
||||
)
|
||||
|
||||
step_index += 1
|
||||
socketio.emit('intermediateResult', {
|
||||
'url': os.path.relpath(path), 'metadata': generation_parameters})
|
||||
socketio.emit('progress', {'step': step + 1})
|
||||
socketio.emit(
|
||||
"intermediateResult",
|
||||
{"url": os.path.relpath(path), "metadata": generation_parameters},
|
||||
)
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
def image_done(image, seed):
|
||||
nonlocal generation_parameters
|
||||
nonlocal esrgan_parameters
|
||||
nonlocal gfpgan_parameters
|
||||
nonlocal progress
|
||||
|
||||
step_index = 1
|
||||
|
||||
progress["currentStatus"] = "Generation complete"
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
all_parameters = generation_parameters
|
||||
postprocessing = False
|
||||
|
||||
if esrgan_parameters:
|
||||
progress["currentStatus"] = "Upscaling"
|
||||
progress["currentStatusHasSteps"] = False
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
image = real_esrgan_upscale(
|
||||
image=image,
|
||||
strength=esrgan_parameters['strength'],
|
||||
upsampler_scale=esrgan_parameters['level'],
|
||||
seed=seed
|
||||
strength=esrgan_parameters["strength"],
|
||||
upsampler_scale=esrgan_parameters["level"],
|
||||
seed=seed,
|
||||
)
|
||||
postprocessing = True
|
||||
all_parameters["upscale"] = [esrgan_parameters['level'], esrgan_parameters['strength']]
|
||||
all_parameters["upscale"] = [
|
||||
esrgan_parameters["level"],
|
||||
esrgan_parameters["strength"],
|
||||
]
|
||||
|
||||
if gfpgan_parameters:
|
||||
progress["currentStatus"] = "Fixing faces"
|
||||
progress["currentStatusHasSteps"] = False
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
image = run_gfpgan(
|
||||
image=image,
|
||||
strength=gfpgan_parameters['strength'],
|
||||
strength=gfpgan_parameters["strength"],
|
||||
seed=seed,
|
||||
upsampler_scale=1,
|
||||
)
|
||||
postprocessing = True
|
||||
all_parameters["gfpgan_strength"] = gfpgan_parameters['strength']
|
||||
all_parameters["gfpgan_strength"] = gfpgan_parameters["strength"]
|
||||
|
||||
all_parameters['seed'] = seed
|
||||
all_parameters["seed"] = seed
|
||||
progress["currentStatus"] = "Saving image"
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
path = save_image(image, all_parameters, result_path, postprocessing=postprocessing)
|
||||
metadata = parameters_to_generated_image_metadata(all_parameters)
|
||||
command = parameters_to_command(all_parameters)
|
||||
|
||||
print(f'Image generated: "{path}"')
|
||||
path = save_image(
|
||||
image, command, metadata, result_path, postprocessing=postprocessing
|
||||
)
|
||||
|
||||
print(f'>> Image generated: "{path}"')
|
||||
write_log_message(f'[Generated] "{path}": {command}')
|
||||
|
||||
if progress["totalIterations"] > progress["currentIteration"]:
|
||||
progress["currentStep"] = 1
|
||||
progress["currentIteration"] += 1
|
||||
progress["currentStatus"] = "Iteration finished"
|
||||
progress["currentStatusHasSteps"] = False
|
||||
else:
|
||||
progress["currentStep"] = 0
|
||||
progress["totalSteps"] = 0
|
||||
progress["currentIteration"] = 0
|
||||
progress["totalIterations"] = 0
|
||||
progress["currentStatus"] = "Finished"
|
||||
progress["isProcessing"] = False
|
||||
|
||||
socketio.emit("progressUpdate", progress)
|
||||
eventlet.sleep(0)
|
||||
|
||||
socketio.emit(
|
||||
'result', {'url': os.path.relpath(path), 'type': 'generation', 'metadata': all_parameters})
|
||||
"generationResult",
|
||||
{"url": os.path.relpath(path), "metadata": metadata},
|
||||
)
|
||||
eventlet.sleep(0)
|
||||
|
||||
try:
|
||||
model.prompt2image(
|
||||
generate.prompt2image(
|
||||
**generation_parameters,
|
||||
step_callback=image_progress,
|
||||
image_callback=image_done
|
||||
image_callback=image_done,
|
||||
)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
@ -381,7 +708,7 @@ def generate_images(generation_parameters, esrgan_parameters, gfpgan_parameters)
|
||||
except CanceledException:
|
||||
pass
|
||||
except Exception as e:
|
||||
socketio.emit('error', (str(e)))
|
||||
socketio.emit("error", {"message": (str(e))})
|
||||
print("\n")
|
||||
traceback.print_exc()
|
||||
print("\n")
|
||||
@ -392,6 +719,6 @@ END ADDITIONAL FUNCTIONS
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(f'Starting server at http://{host}:{port}')
|
||||
if __name__ == "__main__":
|
||||
print(f">> Starting server at http://{host}:{port}")
|
||||
socketio.run(app, host=host, port=port)
|
||||
|
57
docker-build/Dockerfile
Normal file
@ -0,0 +1,57 @@
|
||||
FROM debian
|
||||
|
||||
ARG gsd
|
||||
ENV GITHUB_STABLE_DIFFUSION $gsd
|
||||
|
||||
ARG rsd
|
||||
ENV REQS $rsd
|
||||
|
||||
ARG cs
|
||||
ENV CONDA_SUBDIR $cs
|
||||
|
||||
ENV PIP_EXISTS_ACTION="w"
|
||||
|
||||
# TODO: Optimize image size
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
WORKDIR /
|
||||
RUN apt update && apt upgrade -y \
|
||||
&& apt install -y \
|
||||
git \
|
||||
libgl1-mesa-glx \
|
||||
libglib2.0-0 \
|
||||
pip \
|
||||
python3 \
|
||||
&& git clone $GITHUB_STABLE_DIFFUSION
|
||||
|
||||
# Install Anaconda or Miniconda
|
||||
COPY anaconda.sh .
|
||||
RUN bash anaconda.sh -b -u -p /anaconda && /anaconda/bin/conda init bash
|
||||
|
||||
# SD
|
||||
WORKDIR /stable-diffusion
|
||||
RUN source ~/.bashrc \
|
||||
&& conda create -y --name ldm && conda activate ldm \
|
||||
&& conda config --env --set subdir $CONDA_SUBDIR \
|
||||
&& pip3 install -r $REQS \
|
||||
&& pip3 install basicsr facexlib realesrgan \
|
||||
&& mkdir models/ldm/stable-diffusion-v1 \
|
||||
&& ln -s "/data/sd-v1-4.ckpt" models/ldm/stable-diffusion-v1/model.ckpt
|
||||
|
||||
# Face restoreation
|
||||
# by default expected in a sibling directory to stable-diffusion
|
||||
WORKDIR /
|
||||
RUN git clone https://github.com/TencentARC/GFPGAN.git
|
||||
|
||||
WORKDIR /GFPGAN
|
||||
RUN pip3 install -r requirements.txt \
|
||||
&& python3 setup.py develop \
|
||||
&& ln -s "/data/GFPGANv1.3.pth" experiments/pretrained_models/GFPGANv1.3.pth
|
||||
|
||||
WORKDIR /stable-diffusion
|
||||
RUN python3 scripts/preload_models.py
|
||||
|
||||
WORKDIR /
|
||||
COPY entrypoint.sh .
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
10
docker-build/entrypoint.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd /stable-diffusion
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
python3 scripts/dream.py --full_precision -o /data
|
||||
# bash
|
||||
else
|
||||
python3 scripts/dream.py --full_precision -o /data "$@"
|
||||
fi
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 643 KiB After Width: | Height: | Size: 643 KiB |
Before Width: | Height: | Size: 641 KiB After Width: | Height: | Size: 641 KiB |
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 174 KiB |
Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 2.5 MiB |
Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 2.5 MiB |
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 2.3 MiB |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
@ -2,6 +2,8 @@
|
||||
title: Changelog
|
||||
---
|
||||
|
||||
# :octicons-log-16: Changelog
|
||||
|
||||
## v1.13 <small>(in process)</small>
|
||||
|
||||
- Supports a Google Colab notebook for a standalone server running on Google
|
||||
|
@ -1,27 +1,34 @@
|
||||
---
|
||||
title: CLI
|
||||
hide:
|
||||
- toc
|
||||
---
|
||||
|
||||
# :material-bash: CLI
|
||||
|
||||
## **Interactive Command Line Interface**
|
||||
|
||||
The `dream.py` script, located in `scripts/dream.py`, provides an interactive interface to image
|
||||
generation similar to the "dream mothership" bot that Stable AI provided on its Discord server.
|
||||
|
||||
Unlike the txt2img.py and img2img.py scripts provided in the original CompViz/stable-diffusion
|
||||
source code repository, the time-consuming initialization of the AI model initialization only
|
||||
happens once. After that image generation from the command-line interface is very fast.
|
||||
Unlike the `txt2img.py` and `img2img.py` scripts provided in the original
|
||||
[CompVis/stable-diffusion](https://github.com/CompVis/stable-diffusion) source code repository, the
|
||||
time-consuming initialization of the AI model initialization only happens once. After that image
|
||||
generation from the command-line interface is very fast.
|
||||
|
||||
The script uses the readline library to allow for in-line editing, command history (up and down
|
||||
arrows), autocompletion, and more. To help keep track of which prompts generated which images, the
|
||||
The script uses the readline library to allow for in-line editing, command history (++up++ and
|
||||
++down++), autocompletion, and more. To help keep track of which prompts generated which images, the
|
||||
script writes a log file of image names and prompts to the selected output directory.
|
||||
|
||||
In addition, as of version 1.02, it also writes the prompt into the PNG file's metadata where it can
|
||||
be retrieved using scripts/images2prompt.py
|
||||
be retrieved using `scripts/images2prompt.py`
|
||||
|
||||
The script is confirmed to work on Linux, Windows and Mac systems.
|
||||
|
||||
_Note:_ This script runs from the command-line or can be used as a Web application. The Web GUI is
|
||||
currently rudimentary, but a much better replacement is on its way.
|
||||
!!! note
|
||||
|
||||
This script runs from the command-line or can be used as a Web application. The Web GUI is
|
||||
currently rudimentary, but a much better replacement is on its way.
|
||||
|
||||
```bash
|
||||
(ldm) ~/stable-diffusion$ python3 ./scripts/dream.py
|
||||
@ -47,185 +54,197 @@ dream> q
|
||||
00011.png: "there's a fly in my soup" -n6 -g -S 2685670268
|
||||
```
|
||||
|
||||
<p align='center'>
|
||||
<img src="../assets/dream-py-demo.png"/>
|
||||
</p>
|
||||
![dream-py-demo](../assets/dream-py-demo.png)
|
||||
|
||||
The `dream>` prompt's arguments are pretty much identical to those used in the Discord bot, except
|
||||
you don't need to type "!dream" (it doesn't hurt if you do). A significant change is that creation
|
||||
of individual images is now the default unless --grid (-g) is given. A full list is given in [List
|
||||
of prompt arguments] (#list-of-prompt-arguments).
|
||||
of individual images is now the default unless `--grid` (`-g`) is given. A full list is given in
|
||||
[List of prompt arguments](#list-of-prompt-arguments).
|
||||
|
||||
## Arguments
|
||||
|
||||
The script itself also recognizes a series of command-line switches that will change important
|
||||
global defaults, such as the directory for image outputs and the location of the model weight files.
|
||||
|
||||
## List of arguments recognized at the command line
|
||||
### List of arguments recognized at the command line
|
||||
|
||||
These command-line arguments can be passed to dream.py when you first run it from the Windows, Mac
|
||||
These command-line arguments can be passed to `dream.py` when you first run it from the Windows, Mac
|
||||
or Linux command line. Some set defaults that can be overridden on a per-prompt basis (see [List of
|
||||
prompt arguments] (#list-of-prompt-arguments). Others
|
||||
|
||||
| Argument | Shortcut | Default | Description |
|
||||
| :---------------------- | :---------: | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
|
||||
| --help | -h | | Print a concise help message. |
|
||||
| --outdir <path> | -o<path> | outputs/img_samples | Location for generated images. |
|
||||
| --prompt_as_dir | -p | False | Name output directories using the prompt text. |
|
||||
| --from_file <path> | | None | Read list of prompts from a file. Use "-" to read from standard input |
|
||||
| --model <modelname> | | stable-diffusion-1.4 | Loads model specified in configs/models.yaml. Currently one of "stable-diffusion-1.4" or "laion400m" |
|
||||
| --full_precision | -F | False | Run in slower full-precision mode. Needed for Macintosh M1/M2 hardware and some older video cards. |
|
||||
| --web | | False | Start in web server mode |
|
||||
| --host <ip addr> | | localhost | Which network interface web server should listen on. Set to 0.0.0.0 to listen on any. |
|
||||
| --port <port> | | 9090 | Which port web server should listen for requests on. |
|
||||
| --config <path> | | configs/models.yaml | Configuration file for models and their weights. |
|
||||
| --iterations <int> | -n<int> | 1 | How many images to generate per prompt. |
|
||||
| --grid | -g | False | Save all image series as a grid rather than individually. |
|
||||
| --sampler <sampler> | -A<sampler> | k_lms | Sampler to use. Use -h to get list of available samplers. |
|
||||
| --seamless | | False | Create interesting effects by tiling elements of the image. |
|
||||
| --embedding_path <path> | | None | Path to pre-trained embedding manager checkpoints, for custom models |
|
||||
| --gfpgan_dir | | src/gfpgan | Path to where GFPGAN is installed. |
|
||||
| --gfpgan_model_path | | experiments/pretrained_models<br>/GFPGANv1.3.pth | Path to GFPGAN model file, relative to --gfpgan_dir. |
|
||||
| --device <device> | -d<device> | torch.cuda.current_device() | Device to run SD on, e.g. "cuda:0" |
|
||||
| Argument <img width="240" align="right"/> | Shortcut <img width="100" align="right"/> | Default <img width="320" align="right"/> | Description |
|
||||
| ----------------------------------------- | ----------------------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| `--help` | `-h` | | Print a concise help message. |
|
||||
| `--outdir <path>` | `-o<path>` | `outputs/img_samples` | Location for generated images. |
|
||||
| `--prompt_as_dir` | `-p` | `False` | Name output directories using the prompt text. |
|
||||
| `--from_file <path>` | | `None` | Read list of prompts from a file. Use `-` to read from standard input |
|
||||
| `--model <modelname>` | | `stable-diffusion-1.4` | Loads model specified in configs/models.yaml. Currently one of "stable-diffusion-1.4" or "laion400m" |
|
||||
| `--full_precision` | `-F` | `False` | Run in slower full-precision mode. Needed for Macintosh M1/M2 hardware and some older video cards. |
|
||||
| `--web` | | `False` | Start in web server mode |
|
||||
| `--host <ip addr>` | | `localhost` | Which network interface web server should listen on. Set to 0.0.0.0 to listen on any. |
|
||||
| `--port <port>` | | `9090` | Which port web server should listen for requests on. |
|
||||
| `--config <path>` | | `configs/models.yaml` | Configuration file for models and their weights. |
|
||||
| `--iterations <int>` | `-n<int>` | `1` | How many images to generate per prompt. |
|
||||
| `--grid` | `-g` | `False` | Save all image series as a grid rather than individually. |
|
||||
| `--sampler <sampler>` | `-A<sampler>` | `k_lms` | Sampler to use. Use `-h` to get list of available samplers. |
|
||||
| `--seamless` | | `False` | Create interesting effects by tiling elements of the image. |
|
||||
| `--embedding_path <path>` | | `None` | Path to pre-trained embedding manager checkpoints, for custom models |
|
||||
| `--gfpgan_dir` | | `src/gfpgan` | Path to where GFPGAN is installed. |
|
||||
| `--gfpgan_model_path` | | `experiments/pretrained_models/GFPGANv1.3.pth` | Path to GFPGAN model file, relative to `--gfpgan_dir`. |
|
||||
| `--device <device>` | `-d<device>` | `torch.cuda.current_device()` | Device to run SD on, e.g. "cuda:0" |
|
||||
|
||||
#### deprecated
|
||||
|
||||
These arguments are deprecated but still work:
|
||||
|
||||
| Argument | Shortcut | Default | Description |
|
||||
| ---------------- | -------- | ------- | --------------------------------------------------------------- |
|
||||
| --weights <path> | | None | Pth to weights file; use `--model stable-diffusion-1.4` instead |
|
||||
| --laion400m | -l | False | Use older LAION400m weights; use `--model=laion400m` instead |
|
||||
<figure markdown>
|
||||
|
||||
### **A note on path names:**
|
||||
| Argument | Shortcut | Default | Description |
|
||||
| ------------------ | -------- | ------- | --------------------------------------------------------------- |
|
||||
| `--weights <path>` | | `None` | Pth to weights file; use `--model stable-diffusion-1.4` instead |
|
||||
| `--laion400m` | `-l` | `False` | Use older LAION400m weights; use `--model=laion400m` instead |
|
||||
|
||||
On Windows systems, you may run into problems when passing the dream script standard backslashed
|
||||
path names because the Python interpreter treats "\" as an escape. You can either double your
|
||||
slashes (ick): `C:\\\\path\\\\to\\\\my\\\\file`, or use Linux/Mac style forward slashes (better):
|
||||
`C:/path/to/my/file`.
|
||||
</figure>
|
||||
|
||||
!!! note
|
||||
|
||||
On Windows systems, you may run into problems when passing the dream script standard backslashed
|
||||
path names because the Python interpreter treats `\` as an escape. You can either double your
|
||||
slashes (ick): `C:\\path\\to\\my\\file`, or use Linux/Mac style forward slashes (better):
|
||||
`C:/path/to/my/file`.
|
||||
|
||||
### List of prompt arguments
|
||||
|
||||
After the dream.py script initializes, it will present you with a **dream>** prompt. Here you can
|
||||
enter information to generate images from text (txt2img), to embellish an existing image or sketch
|
||||
(img2img), or to selectively alter chosen regions of the image (inpainting).
|
||||
After the `dream.py` script initializes, it will present you with a **`dream>`** prompt. Here you
|
||||
can enter information to generate images from text (txt2img), to embellish an existing image or
|
||||
sketch (img2img), or to selectively alter chosen regions of the image (inpainting).
|
||||
|
||||
### This is an example of txt2img
|
||||
#### txt2img
|
||||
|
||||
```bash
|
||||
dream> "waterfall and rainbow" -W640 -H480
|
||||
```
|
||||
!!! example
|
||||
|
||||
This will create the requested image with the dimensions 640 (width) and 480 (height).
|
||||
```bash
|
||||
dream> "waterfall and rainbow" -W640 -H480
|
||||
```
|
||||
|
||||
This will create the requested image with the dimensions 640 (width) and 480 (height).
|
||||
|
||||
Those are the `dream` commands that apply to txt2img:
|
||||
|
||||
| Argument | Shortcut | Default | Description |
|
||||
| --------------------------- | ---------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| "my prompt" | | | Text prompt to use. The quotation marks are optional. |
|
||||
| --width <int> | -W<int> | 512 | Width of generated image |
|
||||
| --height <int> | -H<int> | 512 | Height of generated image |
|
||||
| --iterations <int> | -n<int> | 1 | How many images to generate from this prompt |
|
||||
| --steps <int> | -s<int> | 50 | How many steps of refinement to apply |
|
||||
| --cfg_scale <float> | -C<float> | 7.5 | How hard to try to match the prompt to the generated image; any number greater than 0.0 works, but the useful range is roughly 5.0 to 20.0 |
|
||||
| --seed <int> | -S<int> | None | Set the random seed for the next series of images. This can be used to recreate an image generated previously. |
|
||||
| --sampler <sampler> | -A<sampler> | k_lms | Sampler to use. Use -h to get list of available samplers. |
|
||||
| --grid | -g | False | Turn on grid mode to return a single image combining all the images generated by this prompt |
|
||||
| --individual | -i | True | Turn off grid mode (deprecated; leave off --grid instead) |
|
||||
| --outdir <path> | -o<path> | outputs/img_samples | Temporarily change the location of these images |
|
||||
| --seamless | | False | Activate seamless tiling for interesting effects |
|
||||
| --log_tokenization | -t | False | Display a color-coded list of the parsed tokens derived from the prompt |
|
||||
| --skip_normalization | -x | False | Weighted subprompts will not be normalized. See [Weighted Prompts](./OTHER.md#weighted-prompts) |
|
||||
| --upscale <int> <float> | -U <int> <float> | -U 1 0.75 | Upscale image by magnification factor (2, 4), and set strength of upscaling (0.0-1.0). If strength not set, will default to 0.75. |
|
||||
| --gfpgan_strength <float> | -G <float> | -G0 | Fix faces using the GFPGAN algorithm; argument indicates how hard the algorithm should try (0.0-1.0) |
|
||||
| --save_original | -save_orig | False | When upscaling or fixing faces, this will cause the original image to be saved rather than replaced. |
|
||||
| --variation <float> | -v<float> | 0.0 | Add a bit of noise (0.0=none, 1.0=high) to the image in order to generate a series of variations. Usually used in combination with -S<seed> and -n<int> to generate a series a riffs on a starting image. See [Variations](./VARIATIONS.md). |
|
||||
| --with_variations <pattern> | -V<pattern> | None | Combine two or more variations. See [Variations](./VARIATIONS.md) for now to use this. |
|
||||
| Argument <img width="680" align="right"/> | Shortcut <img width="420" align="right"/> | Default <img width="480" align="right"/> | Description |
|
||||
| ----------------------------------------- | ----------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `"my prompt"` | | | Text prompt to use. The quotation marks are optional. |
|
||||
| `--width <int>` | `-W<int>` | `512` | Width of generated image |
|
||||
| `--height <int>` | `-H<int>` | `512` | Height of generated image |
|
||||
| `--iterations <int>` | `-n<int>` | `1` | How many images to generate from this prompt |
|
||||
| `--steps <int>` | `-s<int>` | `50` | How many steps of refinement to apply |
|
||||
| `--cfg_scale <float>` | `-C<float>` | `7.5` | How hard to try to match the prompt to the generated image; any number greater than 0.0 works, but the useful range is roughly 5.0 to 20.0 |
|
||||
| `--seed <int>` | `-S<int>` | `None` | Set the random seed for the next series of images. This can be used to recreate an image generated previously. |
|
||||
| `--sampler <sampler>` | `-A<sampler>` | `k_lms` | Sampler to use. Use `-h` to get list of available samplers. |
|
||||
| `--grid` | `-g` | `False` | Turn on grid mode to return a single image combining all the images generated by this prompt |
|
||||
| `--individual` | `-i` | `True` | Turn off grid mode (deprecated; leave off `--grid` instead) |
|
||||
| `--outdir <path>` | `-o<path>` | `outputs/img_samples` | Temporarily change the location of these images |
|
||||
| `--seamless` | | `False` | Activate seamless tiling for interesting effects |
|
||||
| `--log_tokenization` | `-t` | `False` | Display a color-coded list of the parsed tokens derived from the prompt |
|
||||
| `--skip_normalization` | `-x` | `False` | Weighted subprompts will not be normalized. See [Weighted Prompts](./OTHER.md#weighted-prompts) |
|
||||
| `--upscale <int> <float>` | `-U <int> <float>` | `-U 1 0.75` | Upscale image by magnification factor (2, 4), and set strength of upscaling (0.0-1.0). If strength not set, will default to 0.75. |
|
||||
| `--gfpgan_strength <float>` | `-G <float>` | `-G0` | Fix faces using the GFPGAN algorithm; argument indicates how hard the algorithm should try (0.0-1.0) |
|
||||
| `--save_original` | `-save_orig` | `False` | When upscaling or fixing faces, this will cause the original image to be saved rather than replaced. |
|
||||
| `--variation <float>` | `-v<float>` | `0.0` | Add a bit of noise (0.0=none, 1.0=high) to the image in order to generate a series of variations. Usually used in combination with `-S<seed>` and `-n<int>` to generate a series a riffs on a starting image. See [Variations](./VARIATIONS.md). |
|
||||
| `--with_variations <pattern>` | `-V<pattern>` | `None` | Combine two or more variations. See [Variations](./VARIATIONS.md) for now to use this. |
|
||||
|
||||
Note that the width and height of the image must be multiples of 64. You can provide different
|
||||
values, but they will be rounded down to the nearest multiple of 64.
|
||||
!!! note
|
||||
|
||||
### This is an example of img2img
|
||||
The width and height of the image must be multiples of 64. You can provide different
|
||||
values, but they will be rounded down to the nearest multiple of 64.
|
||||
|
||||
```bash
|
||||
dream> waterfall and rainbow -I./vacation-photo.png -W640 -H480 --fit
|
||||
```
|
||||
#### img2img
|
||||
|
||||
This will modify the indicated vacation photograph by making it more like the prompt. Results will
|
||||
vary greatly depending on what is in the image. We also ask to --fit the image into a box no bigger
|
||||
than 640x480. Otherwise the image size will be identical to the provided photo and you may run out
|
||||
of memory if it is large.
|
||||
!!! example
|
||||
|
||||
Repeated chaining of img2img on an image can result in significant color shifts
|
||||
in the output, especially if run with lower strength. Color correction can be
|
||||
run against a reference image to fix this issue. Use the original input image to the
|
||||
chain as the the reference image for each step in the chain.
|
||||
```bash
|
||||
dream> "waterfall and rainbow" -I./vacation-photo.png -W640 -H480 --fit
|
||||
```
|
||||
|
||||
This will modify the indicated vacation photograph by making it more like the prompt. Results will
|
||||
vary greatly depending on what is in the image. We also ask to --fit the image into a box no bigger
|
||||
than 640x480. Otherwise the image size will be identical to the provided photo and you may run out
|
||||
of memory if it is large.
|
||||
|
||||
Repeated chaining of img2img on an image can result in significant color shifts in the output,
|
||||
especially if run with lower strength. Color correction can be run against a reference image to fix
|
||||
this issue. Use the original input image to the chain as the the reference image for each step in
|
||||
the chain.
|
||||
|
||||
In addition to the command-line options recognized by txt2img, img2img accepts additional options:
|
||||
|
||||
| Argument | Shortcut | Default | Description |
|
||||
| ------------------ | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| --init_img <path> | -I<path> | None | Path to the initialization image |
|
||||
| --init_color <path> | | None | Path to reference image for color correction |
|
||||
| --fit | -F | False | Scale the image to fit into the specified -H and -W dimensions |
|
||||
| --strength <float> | -s<float> | 0.75 | How hard to try to match the prompt to the initial image. Ranges from 0.0-0.99, with higher values replacing the initial image completely. |
|
||||
| Argument <img width="160" align="right"/> | Shortcut | Default | Description |
|
||||
| ----------------------------------------- | ----------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `--init_img <path>` | `-I<path>` | `None` | Path to the initialization image |
|
||||
| `--init_color <path>` | | `None` | Path to reference image for color correction |
|
||||
| `--fit` | `-F` | `False` | Scale the image to fit into the specified -H and -W dimensions |
|
||||
| `--strength <float>` | `-f<float>` | `0.75` | How hard to try to match the prompt to the initial image. Ranges from 0.0-0.99, with higher values replacing the initial image completely. |
|
||||
|
||||
### This is an example of inpainting
|
||||
#### Inpainting
|
||||
|
||||
```bash
|
||||
dream> "waterfall and rainbow" -I./vacation-photo.png -M./vacation-mask.png -W640 -H480 --fit
|
||||
```
|
||||
!!! example
|
||||
|
||||
This will do the same thing as img2img, but image alterations will only occur within transparent
|
||||
areas defined by the mask file specified by -M. You may also supply just a single initial image with
|
||||
the areas to overpaint made transparent, but you must be careful not to destroy the pixels
|
||||
underneath when you create the transparent areas. See [Inpainting](./INPAINTING.md) for details.
|
||||
```bash
|
||||
dream> "waterfall and rainbow" -I./vacation-photo.png -M./vacation-mask.png -W640 -H480 --fit
|
||||
```
|
||||
|
||||
inpainting accepts all the arguments used for txt2img and img2img, as well as the --mask (-M)
|
||||
This will do the same thing as img2img, but image alterations will only occur within transparent
|
||||
areas defined by the mask file specified by `-M`. You may also supply just a single initial image with
|
||||
the areas to overpaint made transparent, but you must be careful not to destroy the pixels
|
||||
underneath when you create the transparent areas. See [Inpainting](./INPAINTING.md) for details.
|
||||
|
||||
Inpainting accepts all the arguments used for txt2img and img2img, as well as the `--mask` (`-M`)
|
||||
argument:
|
||||
|
||||
| Argument | Shortcut | Default | Description |
|
||||
| ------------------ | -------- | ------- | ------------------------------------------------------------------------------------------------ |
|
||||
| --init_mask <path> | -M<path> | None | Path to an image the same size as the initial_image, with areas for inpainting made transparent. |
|
||||
| Argument <img width="100" align="right"/> | Shortcut | Default | Description |
|
||||
| ----------------------------------------- | ---------- | ------- | ------------------------------------------------------------------------------------------------ |
|
||||
| `--init_mask <path>` | `-M<path>` | `None` | Path to an image the same size as the initial_image, with areas for inpainting made transparent. |
|
||||
|
||||
## Command-line editing and completion
|
||||
|
||||
If you are on a Macintosh or Linux machine, the command-line offers convenient history tracking,
|
||||
editing, and command completion.
|
||||
|
||||
- To scroll through previous commands and potentially edit/reuse them, use the up and down cursor
|
||||
keys.
|
||||
- To edit the current command, use the left and right cursor keys to position the cursor, and then
|
||||
backspace, delete or insert characters.
|
||||
- To move to the very beginning of the command, type CTRL-A (or command-A on the Mac)
|
||||
- To move to the end of the command, type CTRL-E.
|
||||
- To scroll through previous commands and potentially edit/reuse them, use the ++up++ and ++down++
|
||||
cursor keys.
|
||||
- To edit the current command, use the ++left++ and ++right++ cursor keys to position the cursor,
|
||||
and then ++backspace++, ++delete++ or ++insert++ characters.
|
||||
- To move to the very beginning of the command, type ++ctrl+a++ (or ++command+a++ on the Mac)
|
||||
- To move to the end of the command, type ++ctrl+e++.
|
||||
- To cut a section of the command, position the cursor where you want to start cutting and type
|
||||
CTRL-K.
|
||||
- To paste a cut section back in, position the cursor where you want to paste, and type CTRL-Y
|
||||
++ctrl+k++.
|
||||
- To paste a cut section back in, position the cursor where you want to paste, and type ++ctrl+y++
|
||||
|
||||
Windows users can get similar, but more limited, functionality if they launch dream.py with the
|
||||
Windows users can get similar, but more limited, functionality if they launch `dream.py` with the
|
||||
"winpty" program:
|
||||
|
||||
```
|
||||
> winpty python scripts\dream.py
|
||||
```batch
|
||||
winpty python scripts\dream.py
|
||||
```
|
||||
|
||||
On the Mac and Linux platforms, when you exit dream.py, the last 1000 lines of your command-line
|
||||
history will be saved. When you restart dream.py, you can access the saved history using the
|
||||
up-arrow key.
|
||||
On the Mac and Linux platforms, when you exit `dream.py`, the last 1000 lines of your command-line
|
||||
history will be saved. When you restart `dream.py`, you can access the saved history using the
|
||||
++up++ key.
|
||||
|
||||
In addition, limited command-line completion is installed. In various contexts, you can start typing
|
||||
your command and press tab. A list of potential completions will be presented to you. You can then
|
||||
type a little more, hit tab again, and eventually autocomplete what you want.
|
||||
|
||||
When specifying file paths using the one-letter shortcuts, the CLI will attempt to complete
|
||||
pathnames for you. This is most handy for the -I (init image) and -M (init mask) paths. To initiate
|
||||
completion, start the path with a slash ("/") or "./". For example:
|
||||
pathnames for you. This is most handy for the `-I` (init image) and `-M` (init mask) paths. To
|
||||
initiate completion, start the path with a slash `/` or `./`, for example:
|
||||
|
||||
```
|
||||
dream> zebra with a mustache -I./test-pictures<TAB>
|
||||
```bash
|
||||
dream> "zebra with a mustache" -I./test-pictures<TAB>
|
||||
-I./test-pictures/Lincoln-and-Parrot.png -I./test-pictures/zebra.jpg -I./test-pictures/madonna.png
|
||||
-I./test-pictures/bad-sketch.png -I./test-pictures/man_with_eagle/
|
||||
```
|
||||
|
||||
You can then type "z", hit tab again, and it will autofill to "zebra.jpg".
|
||||
You can then type ++z++, hit ++tab++ again, and it will autofill to `zebra.jpg`.
|
||||
|
||||
More text completion features (such as autocompleting seeds) are on their way.
|
||||
|
@ -1,4 +1,10 @@
|
||||
# **Embiggen -- upscale your images on limited memory machines**
|
||||
---
|
||||
title: Embiggen
|
||||
---
|
||||
|
||||
# :material-loupe: Embiggen
|
||||
|
||||
**upscale your images on limited memory machines**
|
||||
|
||||
GFPGAN and Real-ESRGAN are both memory intensive. In order to avoid
|
||||
crashes and memory overloads during the Stable Diffusion process,
|
||||
@ -16,7 +22,7 @@ face restore a particular generated image, pass it again with the same
|
||||
prompt and generated seed along with the `-U` and `-G` prompt
|
||||
arguments to perform those actions.
|
||||
|
||||
## Embiggen
|
||||
## Embiggen
|
||||
|
||||
If you wanted to be able to do more (pixels) without running out of VRAM,
|
||||
or you want to upscale with details that couldn't possibly appear
|
||||
@ -37,7 +43,7 @@ it's similar to that, except it can work up to an arbitrarily large size
|
||||
has extra logic to re-run any number of the tile sub-sections of the image
|
||||
if for example a small part of a huge run got messed up.
|
||||
|
||||
**Usage**
|
||||
## Usage
|
||||
|
||||
`-embiggen <scaling_factor> <esrgan_strength> <overlap_ratio OR overlap_pixels>`
|
||||
|
||||
@ -94,12 +100,12 @@ Tiles are numbered starting with one, and left-to-right,
|
||||
top-to-bottom. So, if you are generating a 3x3 tiled image, the
|
||||
middle row would be `4 5 6`.
|
||||
|
||||
**Example Usage**
|
||||
## Example Usage
|
||||
|
||||
Running Embiggen with 512x512 tiles on an existing image, scaling up by a factor of 2.5x;
|
||||
and doing the same again (default ESRGAN strength is 0.75, default overlap between tiles is 0.25):
|
||||
|
||||
```
|
||||
```bash
|
||||
dream > a photo of a forest at sunset -s 100 -W 512 -H 512 -I outputs/forest.png -f 0.4 -embiggen 2.5
|
||||
dream > a photo of a forest at sunset -s 100 -W 512 -H 512 -I outputs/forest.png -f 0.4 -embiggen 2.5 0.75 0.25
|
||||
```
|
||||
@ -111,23 +117,23 @@ If there weren't enough clouds in the sky of that forest you just made
|
||||
512x512 tiles with 0.25 overlaps wide) we can replace that top row of
|
||||
tiles:
|
||||
|
||||
```
|
||||
```bash
|
||||
dream> a photo of puffy clouds over a forest at sunset -s 100 -W 512 -H 512 -I outputs/000002.seed.png -f 0.5 -embiggen_tiles 1 2 3
|
||||
```
|
||||
|
||||
**Note**
|
||||
!!! note
|
||||
|
||||
Because the same prompt is used on all the tiled images, and the model
|
||||
doesn't have the context of anything outside the tile being run - it
|
||||
can end up creating repeated pattern (also called 'motifs') across all
|
||||
the tiles based on that prompt. The best way to combat this is
|
||||
lowering the `--strength` (`-f`) to stay more true to the init image,
|
||||
and increasing the number of steps so there is more compute-time to
|
||||
create the detail. Anecdotally `--strength` 0.35-0.45 works pretty
|
||||
well on most things. It may also work great in some examples even with
|
||||
the `--strength` set high for patterns, landscapes, or subjects that
|
||||
are more abstract. Because this is (relatively) fast, you can also
|
||||
always create a few Embiggen'ed images and manually composite them to
|
||||
preserve the best parts from each.
|
||||
Because the same prompt is used on all the tiled images, and the model
|
||||
doesn't have the context of anything outside the tile being run - it
|
||||
can end up creating repeated pattern (also called 'motifs') across all
|
||||
the tiles based on that prompt. The best way to combat this is
|
||||
lowering the `--strength` (`-f`) to stay more true to the init image,
|
||||
and increasing the number of steps so there is more compute-time to
|
||||
create the detail. Anecdotally `--strength` 0.35-0.45 works pretty
|
||||
well on most things. It may also work great in some examples even with
|
||||
the `--strength` set high for patterns, landscapes, or subjects that
|
||||
are more abstract. Because this is (relatively) fast, you can also
|
||||
always create a few Embiggen'ed images and manually composite them to
|
||||
preserve the best parts from each.
|
||||
|
||||
Author: [Travco](https://github.com/travco)
|
||||
Author: [Travco](https://github.com/travco)
|
||||
|
@ -2,7 +2,8 @@
|
||||
title: Image-to-Image
|
||||
---
|
||||
|
||||
## **IMG2IMG**
|
||||
# :material-image-multiple: **IMG2IMG**
|
||||
|
||||
This script also provides an `img2img` feature that lets you seed your creations with an initial
|
||||
drawing or photo. This is a really cool feature that tells stable diffusion to build the prompt on
|
||||
top of the image you provide, preserving the original's basic shape and layout. To use it, provide
|
||||
|
@ -2,6 +2,8 @@
|
||||
title: Inpainting
|
||||
---
|
||||
|
||||
# :octicons-paintbrush-16: Inpainting
|
||||
|
||||
## **Creating Transparent Regions for Inpainting**
|
||||
|
||||
Inpainting is really cool. To do it, you start with an initial image and use a photoeditor to make
|
||||
@ -26,6 +28,8 @@ dream> "man with cat on shoulder" -I./images/man.png -M./images/man-transparent.
|
||||
|
||||
We are hoping to get rid of the need for this workaround in an upcoming release.
|
||||
|
||||
---
|
||||
|
||||
## Recipe for GIMP
|
||||
|
||||
[GIMP](https://www.gimp.org/) is a popular Linux photoediting tool.
|
||||
@ -34,7 +38,7 @@ We are hoping to get rid of the need for this workaround in an upcoming release.
|
||||
2. Layer->Transparency->Add Alpha Channel
|
||||
3. Use lasoo tool to select region to mask
|
||||
4. Choose Select -> Float to create a floating selection
|
||||
5. Open the Layers toolbar (^L) and select "Floating Selection"
|
||||
5. Open the Layers toolbar (++ctrl+l++) and select "Floating Selection"
|
||||
6. Set opacity to 0%
|
||||
7. Export as PNG
|
||||
8. In the export dialogue, Make sure the "Save colour values from
|
||||
@ -44,37 +48,41 @@ We are hoping to get rid of the need for this workaround in an upcoming release.
|
||||
## Recipe for Adobe Photoshop
|
||||
|
||||
1. Open image in Photoshop
|
||||
<p align='left'>
|
||||
<img src="../assets/step1.png"/>
|
||||
</p>
|
||||
|
||||
<figure markdown>
|
||||
![step1](../assets/step1.png)
|
||||
</figure>
|
||||
|
||||
2. Use any of the selection tools (Marquee, Lasso, or Wand) to select the area you desire to inpaint.
|
||||
<p align='left'>
|
||||
<img src="../assets/step2.png"/>
|
||||
</p>
|
||||
|
||||
3. Because we'll be applying a mask over the area we want to preserve, you should now select the inverse by using the Shift + Ctrl + I shortcut, or right clicking and using the "Select Inverse" option.
|
||||
<figure markdown>
|
||||
![step2](../assets/step2.png)
|
||||
</figure>
|
||||
|
||||
4. You'll now create a mask by selecting the image layer, and Masking the selection. Make sure that you don't delete any of the underlying image, or your inpainting results will be dramatically impacted.
|
||||
<p align='left'>
|
||||
<img src="../assets/step4.png"/>
|
||||
</p>
|
||||
3. Because we'll be applying a mask over the area we want to preserve, you should now select the inverse by using the ++shift+ctrl+i++ shortcut, or right clicking and using the "Select Inverse" option.
|
||||
|
||||
4. You'll now create a mask by selecting the image layer, and Masking the selection. Make sure that you don't delete any of the undrlying image, or your inpainting results will be dramatically impacted.
|
||||
|
||||
<figure markdown>
|
||||
![step4](../assets/step4.png)
|
||||
</figure>
|
||||
|
||||
5. Make sure to hide any background layers that are present. You should see the mask applied to your image layer, and the image on your canvas should display the checkered background.
|
||||
<p align='left'>
|
||||
<img src="../assets/step5.png"/>
|
||||
</p>
|
||||
|
||||
<p align='left'>
|
||||
<img src="../assets/step6.png"/>
|
||||
</p>
|
||||
<figure markdown>
|
||||
![step5](../assets/step5.png)
|
||||
</figure>
|
||||
|
||||
6. Save the image as a transparent PNG by using the "Save a Copy" option in the File menu, or using the Alt + Ctrl + S keyboard shortcut.
|
||||
6. Save the image as a transparent PNG by using the "Save a Copy" option in the File menu, or using the Alt + Ctrl + S keyboard shortcut
|
||||
|
||||
<figure markdown>
|
||||
![step6](../assets/step6.png)
|
||||
</figure>
|
||||
|
||||
7. After following the inpainting instructions above (either through the CLI or the Web UI), marvel at your newfound ability to selectively dream. Lookin' good!
|
||||
<p align='left'>
|
||||
<img src="../assets/step7.png"/>
|
||||
</p>
|
||||
|
||||
8. In the export dialogue, Make sure the "Save colour values from transparent pixels" checkbox is
|
||||
selected.
|
||||
<figure markdown>
|
||||
![step7](../assets/step7.png)
|
||||
</figure>
|
||||
|
||||
8. In the export dialogue, Make sure the "Save colour values from transparent pixels" checkbox is selected.
|
||||
|
@ -2,6 +2,8 @@
|
||||
title: Others
|
||||
---
|
||||
|
||||
# :fontawesome-regular-share-from-square: Others
|
||||
|
||||
## **Google Colab**
|
||||
|
||||
Stable Diffusion AI Notebook: <a
|
||||
|
@ -1,4 +1,8 @@
|
||||
# Prompting Features
|
||||
---
|
||||
title: Prompting Features
|
||||
---
|
||||
|
||||
# :octicons-command-palette-24: Prompting Features
|
||||
|
||||
## **Reading Prompts from a File**
|
||||
|
||||
@ -54,43 +58,41 @@ In the above statement, the words 'not really cool` will be ignored by Stable Di
|
||||
|
||||
Here's a prompt that depicts what it does.
|
||||
|
||||
original prompt:
|
||||
original prompt:
|
||||
|
||||
```bash
|
||||
"A fantastical translucent poney made of water and foam, ethereal, radiant, hyperalism, scottish folklore, digital painting, artstation, concept art, smooth, 8 k frostbite 3 engine, ultra detailed, art by artgerm and greg rutkowski and magali villeneuve" -s 20 -W 512 -H 768 -C 7.5 -A k_euler_a -S 1654590180
|
||||
```
|
||||
`#!bash "A fantastical translucent poney made of water and foam, ethereal, radiant, hyperalism, scottish folklore, digital painting, artstation, concept art, smooth, 8 k frostbite 3 engine, ultra detailed, art by artgerm and greg rutkowski and magali villeneuve" -s 20 -W 512 -H 768 -C 7.5 -A k_euler_a -S 1654590180`
|
||||
|
||||
![step1](../assets/variation_walkthru/step1.png)
|
||||
<figure markdown>
|
||||
![step1](../assets/negative_prompt_walkthru/step1.png)
|
||||
</figure>
|
||||
|
||||
That image has a woman, so if we want the horse without a rider, we can influence the image not to have a woman by putting [woman] in the prompt, like this:
|
||||
|
||||
```bash
|
||||
"A fantastical translucent poney made of water and foam, ethereal, radiant, hyperalism, scottish folklore, digital painting, artstation, concept art, smooth, 8 k frostbite 3 engine, ultra detailed, art by artgerm and greg rutkowski and magali villeneuve [woman]" -s 20 -W 512 -H 768 -C 7.5 -A k_euler_a -S 1654590180
|
||||
```
|
||||
`#!bash "A fantastical translucent poney made of water and foam, ethereal, radiant, hyperalism, scottish folklore, digital painting, artstation, concept art, smooth, 8 k frostbite 3 engine, ultra detailed, art by artgerm and greg rutkowski and magali villeneuve [woman]" -s 20 -W 512 -H 768 -C 7.5 -A k_euler_a -S 1654590180`
|
||||
|
||||
![step2](../assets/variation_walkthru/step2.png)
|
||||
<figure markdown>
|
||||
![step2](../assets/negative_prompt_walkthru/step2.png)
|
||||
</figure>
|
||||
|
||||
That's nice - but say we also don't want the image to be quite so blue. We can add "blue" to the list of negative prompts, so it's now [woman blue]:
|
||||
|
||||
```bash
|
||||
"A fantastical translucent poney made of water and foam, ethereal, radiant, hyperalism, scottish folklore, digital painting, artstation, concept art, smooth, 8 k frostbite 3 engine, ultra detailed, art by artgerm and greg rutkowski and magali villeneuve [woman blue]" -s 20 -W 512 -H 768 -C 7.5 -A k_euler_a -S 1654590180
|
||||
```
|
||||
|
||||
![step3](../assets/variation_walkthru/step3.png)
|
||||
`#!bash "A fantastical translucent poney made of water and foam, ethereal, radiant, hyperalism, scottish folklore, digital painting, artstation, concept art, smooth, 8 k frostbite 3 engine, ultra detailed, art by artgerm and greg rutkowski and magali villeneuve [woman blue]" -s 20 -W 512 -H 768 -C 7.5 -A k_euler_a -S 1654590180`
|
||||
|
||||
<figure markdown>
|
||||
![step3](../assets/negative_prompt_walkthru/step3.png)
|
||||
</figure>
|
||||
|
||||
Getting close - but there's no sense in having a saddle when our horse doesn't have a rider, so we'll add one more negative prompt: [woman blue saddle].
|
||||
|
||||
```bash
|
||||
"A fantastical translucent poney made of water and foam, ethereal, radiant, hyperalism, scottish folklore, digital painting, artstation, concept art, smooth, 8 k frostbite 3 engine, ultra detailed, art by artgerm and greg rutkowski and magali villeneuve [woman blue saddle]" -s 20 -W 512 -H 768 -C 7.5 -A k_euler_a -S 1654590180
|
||||
```
|
||||
`#!bash "A fantastical translucent poney made of water and foam, ethereal, radiant, hyperalism, scottish folklore, digital painting, artstation, concept art, smooth, 8 k frostbite 3 engine, ultra detailed, art by artgerm and greg rutkowski and magali villeneuve [woman blue saddle]" -s 20 -W 512 -H 768 -C 7.5 -A k_euler_a -S 1654590180`
|
||||
|
||||
![step4](../assets/variation_walkthru/step4.png)
|
||||
<figure markdown>
|
||||
![step4](../assets/negative_prompt_walkthru/step4.png)
|
||||
</figure>
|
||||
|
||||
!!! notes "Notes about this feature:"
|
||||
|
||||
Notes about this feature:
|
||||
|
||||
* The only requirement for words to be ignored is that they are in between a pair of square brackets.
|
||||
* You can provide multiple words within the same bracket.
|
||||
* You can provide multiple brackets with multiple words in different places of your prompt. That works just fine.
|
||||
* To improve typical anatomy problems, you can add negative prompts like [bad anatomy, extra legs, extra arms, extra fingers, poorly drawn hands, poorly drawn feet, disfigured, out of frame, tiling, bad art, deformed, mutated].
|
||||
* The only requirement for words to be ignored is that they are in between a pair of square brackets.
|
||||
* You can provide multiple words within the same bracket.
|
||||
* You can provide multiple brackets with multiple words in different places of your prompt. That works just fine.
|
||||
* To improve typical anatomy problems, you can add negative prompts like `[bad anatomy, extra legs, extra arms, extra fingers, poorly drawn hands, poorly drawn feet, disfigured, out of frame, tiling, bad art, deformed, mutated]`.
|
||||
|
@ -2,6 +2,8 @@
|
||||
title: TEXTUAL_INVERSION
|
||||
---
|
||||
|
||||
# :material-file-document-plus-outline: TEXTUAL_INVERSION
|
||||
|
||||
## **Personalizing Text-to-Image Generation**
|
||||
|
||||
You may personalize the generated images to provide your own styles or objects
|
||||
@ -39,7 +41,7 @@ and one with the init word provided.
|
||||
|
||||
On a RTX3090, the process for SD will take ~1h @1.6 iterations/sec.
|
||||
|
||||
!!! Info _Note_
|
||||
!!! note
|
||||
|
||||
According to the associated paper, the optimal number of
|
||||
images is 3-5. Your model may not converge if you use more images than
|
||||
@ -57,9 +59,7 @@ Once the model is trained, specify the trained .pt or .bin file when starting
|
||||
dream using
|
||||
|
||||
```bash
|
||||
python3 ./scripts/dream.py \
|
||||
--embedding_path /path/to/embedding.pt \
|
||||
--full_precision
|
||||
python3 ./scripts/dream.py --embedding_path /path/to/embedding.pt
|
||||
```
|
||||
|
||||
Then, to utilize your subject at the dream prompt
|
||||
|
@ -4,14 +4,16 @@ title: Upscale
|
||||
|
||||
## **Intro**
|
||||
|
||||
The script provides the ability to restore faces and upscale.
|
||||
The script provides the ability to restore faces and upscale. You can apply these operations
|
||||
at the time you generate the images, or at any time to a previously-generated PNG file, using
|
||||
the [!fix](#Fixing Previously-Generated Images) command.
|
||||
|
||||
You can enable these features by passing `--restore` and `--esrgan` to your launch script to enable
|
||||
face restoration modules and upscaling modules respectively.
|
||||
# :material-image-size-select-large: Upscale
|
||||
|
||||
## **GFPGAN and Real-ESRGAN Support**
|
||||
## **Face Fixing**
|
||||
|
||||
The default face restoration module is GFPGAN and the default upscaling module is ESRGAN.
|
||||
The default face restoration module is GFPGAN. The default upscale is Real-ESRGAN. For an alternative
|
||||
face restoration module, see [CodeFormer Support] below.
|
||||
|
||||
As of version 1.14, environment.yaml will install the Real-ESRGAN package into the standard install
|
||||
location for python packages, and will put GFPGAN into a subdirectory of "src" in the
|
||||
@ -36,11 +38,13 @@ this package which asked you to install GFPGAN in a sibling directory, you may u
|
||||
`--gfpgan_dir` argument with `dream.py` to set a custom path to your GFPGAN directory. _There are
|
||||
other GFPGAN related boot arguments if you wish to customize further._
|
||||
|
||||
**Note: Internet connection needed:** Users whose GPU machines are isolated from the Internet (e.g.
|
||||
on a University cluster) should be aware that the first time you run dream.py with GFPGAN and
|
||||
Real-ESRGAN turned on, it will try to download model files from the Internet. To rectify this, you
|
||||
may run `python3 scripts/preload_models.py` after you have installed GFPGAN and all its
|
||||
dependencies.
|
||||
!!! warning "Internet connection needed"
|
||||
|
||||
Users whose GPU machines are isolated from the Internet (e.g.
|
||||
on a University cluster) should be aware that the first time you run dream.py with GFPGAN and
|
||||
Real-ESRGAN turned on, it will try to download model files from the Internet. To rectify this, you
|
||||
may run `python3 scripts/preload_models.py` after you have installed GFPGAN and all its
|
||||
dependencies.
|
||||
|
||||
## **Usage**
|
||||
|
||||
@ -89,16 +93,16 @@ This also works with img2img:
|
||||
dream> a man wearing a pineapple hat -I path/to/your/file.png -U 2 0.5 -G 0.6
|
||||
```
|
||||
|
||||
### **Note**
|
||||
!!! note
|
||||
|
||||
GFPGAN and Real-ESRGAN are both memory intensive. In order to avoid crashes and memory overloads
|
||||
during the Stable Diffusion process, these effects are applied after Stable Diffusion has completed
|
||||
its work.
|
||||
GFPGAN and Real-ESRGAN are both memory intensive. In order to avoid crashes and memory overloads
|
||||
during the Stable Diffusion process, these effects are applied after Stable Diffusion has completed
|
||||
its work.
|
||||
|
||||
In single image generations, you will see the output right away but when you are using multiple
|
||||
iterations, the images will first be generated and then upscaled and face restored after that
|
||||
process is complete. While the image generation is taking place, you will still be able to preview
|
||||
the base images.
|
||||
In single image generations, you will see the output right away but when you are using multiple
|
||||
iterations, the images will first be generated and then upscaled and face restored after that
|
||||
process is complete. While the image generation is taking place, you will still be able to preview
|
||||
the base images.
|
||||
|
||||
If you wish to stop during the image generation but want to upscale or face restore a particular
|
||||
generated image, pass it again with the same prompt and generated seed along with the `-U` and `-G`
|
||||
@ -139,3 +143,22 @@ that is the best restoration possible. This may deviate slightly from the origin
|
||||
excellent option to use in situations when there is very little facial data to work with.
|
||||
|
||||
`<prompt> -G 1.0 -ft codeformer -cf 0.1`
|
||||
|
||||
## Fixing Previously-Generated Images
|
||||
|
||||
It is easy to apply face restoration and/or upscaling to any previously-generated file. Just use the
|
||||
syntax `!fix path/to/file.png <options>`. For example, to apply GFPGAN at strength 0.8 and upscale 2X
|
||||
for a file named `./outputs/img-samples/000044.2945021133.png`, just run:
|
||||
|
||||
~~~~
|
||||
dream> !fix ./outputs/img-samples/000044.2945021133.png -G 0.8 -U 2
|
||||
~~~~
|
||||
|
||||
A new file named `000044.2945021133.fixed.png` will be created in the output directory. Note that
|
||||
the `!fix` command does not replace the original file, unlike the behavior at generate time.
|
||||
|
||||
**Disabling:**
|
||||
|
||||
If, for some reason, you do not wish to load the GFPGAN and/or ESRGAN libraries, you can disable them
|
||||
on the dream.py command line with the `--no_restore` and `--no_upscale` options, respectively.
|
||||
|
||||
|
@ -2,6 +2,10 @@
|
||||
title: Variations
|
||||
---
|
||||
|
||||
# :material-tune-variant: Variations
|
||||
|
||||
## Intro
|
||||
|
||||
Release 1.13 of SD-Dream adds support for image variations.
|
||||
|
||||
You are able to do the following:
|
||||
@ -29,7 +33,7 @@ This will be indicated as `prompt` in the examples below.
|
||||
First we let SD create a series of images in the usual way, in this case
|
||||
requesting six iterations:
|
||||
|
||||
```
|
||||
```bash
|
||||
dream> lucy lawless as xena, warrior princess, character portrait, high resolution -n6
|
||||
...
|
||||
Outputs:
|
||||
@ -41,9 +45,10 @@ Outputs:
|
||||
./outputs/Xena/000001.3357757885.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -S3357757885
|
||||
```
|
||||
|
||||
The one with seed 3357757885 looks nice:
|
||||
|
||||
![var1](../assets/variation_walkthru/000001.3357757885.png)
|
||||
<figure markdown>
|
||||
![var1](../assets/variation_walkthru/000001.3357757885.png)
|
||||
<figcaption>Seed 3357757885 looks nice</figcaption>
|
||||
</figure>
|
||||
|
||||
---
|
||||
|
||||
@ -75,15 +80,21 @@ used to generate it.
|
||||
This gives us a series of closely-related variations, including the two shown
|
||||
here.
|
||||
|
||||
![var2](../assets/variation_walkthru/000002.3647897225.png)
|
||||
<figure markdown>
|
||||
![var2](../assets/variation_walkthru/000002.3647897225.png)
|
||||
<figcaption>subseed 3647897225</figcaption>
|
||||
</figure>
|
||||
|
||||
![var3](../assets/variation_walkthru/000002.1614299449.png)
|
||||
<figure markdown>
|
||||
![var3](../assets/variation_walkthru/000002.1614299449.png)
|
||||
<figcaption>subseed 1614299449</figcaption>
|
||||
</figure>
|
||||
|
||||
I like the expression on Xena's face in the first one (subseed 3647897225), and
|
||||
the armor on her shoulder in the second one (subseed 1614299449). Can we combine
|
||||
them to get the best of both worlds?
|
||||
|
||||
We combine the two variations using `-V` (--with_variations). Again, we must
|
||||
We combine the two variations using `-V` (`--with_variations`). Again, we must
|
||||
provide the seed for the originally-chosen image in order for this to work.
|
||||
|
||||
```bash
|
||||
@ -95,7 +106,9 @@ Outputs:
|
||||
Here we are providing equal weights (0.1 and 0.1) for both the subseeds. The
|
||||
resulting image is close, but not exactly what I wanted:
|
||||
|
||||
![var4](../assets/variation_walkthru/000003.1614299449.png)
|
||||
<figure markdown>
|
||||
![var4](../assets/variation_walkthru/000003.1614299449.png)
|
||||
</figure>
|
||||
|
||||
We could either try combining the images with different weights, or we can
|
||||
generate more variations around the almost-but-not-quite image. We do the
|
||||
@ -116,7 +129,10 @@ Outputs:
|
||||
This produces six images, all slight variations on the combination of the chosen
|
||||
two images. Here's the one I like best:
|
||||
|
||||
![var5](../assets/variation_walkthru/000004.3747154981.png)
|
||||
<figure markdown>
|
||||
![var5](../assets/variation_walkthru/000004.3747154981.png)
|
||||
<figcaption>000004.3747154981.png</figcaption>
|
||||
</figure>
|
||||
|
||||
As you can see, this is a very powerful tool, which when combined with subprompt
|
||||
weighting, gives you great control over the content and quality of your
|
||||
|
@ -2,8 +2,10 @@
|
||||
title: Barebones Web Server
|
||||
---
|
||||
|
||||
# :material-web: Barebones Web Server
|
||||
|
||||
As of version 1.10, this distribution comes with a bare bones web server (see
|
||||
screenshot). To use it, run the `dream.py` script by adding the `**--web**`
|
||||
screenshot). To use it, run the `dream.py` script by adding the `--web`
|
||||
option.
|
||||
|
||||
```bash
|
||||
|
@ -2,6 +2,8 @@
|
||||
title: F.A.Q.
|
||||
---
|
||||
|
||||
# :material-frequently-asked-questions: F.A.Q.
|
||||
|
||||
## **Frequently-Asked-Questions**
|
||||
|
||||
Here are a few common installation problems and their solutions. Often these are caused by
|
||||
|
130
docs/index.md
@ -1,102 +1,106 @@
|
||||
---
|
||||
title: Home
|
||||
template: main.html
|
||||
---
|
||||
|
||||
<!--
|
||||
The Docs you find here (/docs/*) are built and deployed via mkdocs. If you want to do so from local it is pretty strait forward:
|
||||
The Docs you find here (/docs/*) are built and deployed via mkdocs. If you want to run a local version to verify your changes, it's as simple as::
|
||||
|
||||
```bash
|
||||
pip install -r requirements-mkdocs.txt
|
||||
mkdocs serve -a localhost:8080
|
||||
mkdocs serve
|
||||
```
|
||||
-->
|
||||
<div align="center" markdown>
|
||||
|
||||
<h1 align='center'><b>Stable Diffusion Dream Script</b></h1>
|
||||
# :material-script-text-outline: Stable Diffusion Dream Script
|
||||
|
||||
<p align='center'>
|
||||
<img src="./assets/logo.png"/>
|
||||
</p>
|
||||
![project logo](assets/logo.png)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/github/last-commit/lstein/stable-diffusion?logo=Python&logoColor=green&style=for-the-badge" alt="last-commit"/>
|
||||
<img src="https://img.shields.io/github/stars/lstein/stable-diffusion?logo=GitHub&style=for-the-badge" alt="stars"/>
|
||||
<br>
|
||||
<img src="https://img.shields.io/github/issues/lstein/stable-diffusion?logo=GitHub&style=for-the-badge" alt="issues"/>
|
||||
<img src="https://img.shields.io/github/issues-pr/lstein/stable-diffusion?logo=GitHub&style=for-the-badge" alt="pull-requests"/>
|
||||
</p>
|
||||
[![discord badge]][discord link]
|
||||
|
||||
[![latest release badge]][latest release link] [![github stars badge]][github stars link] [![github forks badge]][github forks link]
|
||||
|
||||
[![CI checks on main badge]][CI checks on main link] [![CI checks on dev badge]][CI checks on dev link] [![latest commit to dev badge]][latest commit to dev link]
|
||||
|
||||
[![github open issues badge]][github open issues link] [![github open prs badge]][github open prs link]
|
||||
|
||||
[CI checks on dev badge]: https://flat.badgen.net/github/checks/lstein/stable-diffusion/development?label=CI%20status%20on%20dev&cache=900&icon=github
|
||||
[CI checks on dev link]: https://github.com/lstein/stable-diffusion/actions?query=branch%3Adevelopment
|
||||
[CI checks on main badge]: https://flat.badgen.net/github/checks/lstein/stable-diffusion/main?label=CI%20status%20on%20main&cache=900&icon=github
|
||||
[CI checks on main link]: https://github.com/lstein/stable-diffusion/actions/workflows/test-dream-conda.yml
|
||||
[discord badge]: https://flat.badgen.net/discord/members/htRgbc7e?icon=discord
|
||||
[discord link]: https://discord.com/invite/htRgbc7e
|
||||
[github forks badge]: https://flat.badgen.net/github/forks/lstein/stable-diffusion?icon=github
|
||||
[github forks link]: https://useful-forks.github.io/?repo=lstein%2Fstable-diffusion
|
||||
[github open issues badge]: https://flat.badgen.net/github/open-issues/lstein/stable-diffusion?icon=github
|
||||
[github open issues link]: https://github.com/lstein/stable-diffusion/issues?q=is%3Aissue+is%3Aopen
|
||||
[github open prs badge]: https://flat.badgen.net/github/open-prs/lstein/stable-diffusion?icon=github
|
||||
[github open prs link]: https://github.com/lstein/stable-diffusion/pulls?q=is%3Apr+is%3Aopen
|
||||
[github stars badge]: https://flat.badgen.net/github/stars/lstein/stable-diffusion?icon=github
|
||||
[github stars link]: https://github.com/lstein/stable-diffusion/stargazers
|
||||
[latest commit to dev badge]: https://flat.badgen.net/github/last-commit/lstein/stable-diffusion/development?icon=github&color=yellow&label=last%20dev%20commit&cache=900
|
||||
[latest commit to dev link]: https://github.com/lstein/stable-diffusion/commits/development
|
||||
[latest release badge]: https://flat.badgen.net/github/release/lstein/stable-diffusion/development?icon=github
|
||||
[latest release link]: https://github.com/lstein/stable-diffusion/releases
|
||||
|
||||
</div>
|
||||
|
||||
This is a fork of [CompVis/stable-diffusion](https://github.com/CompVis/stable-diffusion), the open
|
||||
source text-to-image generator. It provides a streamlined process with various new features and
|
||||
options to aid the image generation process. It runs on Windows, Mac and Linux machines, and runs on
|
||||
GPU cards with as little as 4 GB or RAM.
|
||||
|
||||
_Note: This fork is rapidly evolving. Please use the
|
||||
[Issues](https://github.com/lstein/stable-diffusion/issues) tab to report bugs and make feature
|
||||
requests. Be sure to use the provided templates. They will help aid diagnose issues faster._
|
||||
!!! note
|
||||
|
||||
## Installation
|
||||
This fork is rapidly evolving. Please use the
|
||||
[Issues](https://github.com/lstein/stable-diffusion/issues) tab to report bugs and make feature
|
||||
requests. Be sure to use the provided templates. They will help aid diagnose issues faster.
|
||||
|
||||
## :octicons-package-dependencies-24: Installation
|
||||
|
||||
This fork is supported across multiple platforms. You can find individual installation instructions
|
||||
below.
|
||||
|
||||
- [Linux](installation/INSTALL_LINUX.md)
|
||||
- [Windows](installation/INSTALL_WINDOWS.md)
|
||||
- [Macintosh](installation/INSTALL_MAC.md)
|
||||
- :fontawesome-brands-linux: [Linux](installation/INSTALL_LINUX.md)
|
||||
- :fontawesome-brands-windows: [Windows](installation/INSTALL_WINDOWS.md)
|
||||
- :fontawesome-brands-apple: [Macintosh](installation/INSTALL_MAC.md)
|
||||
|
||||
## Hardware Requirements
|
||||
## :fontawesome-solid-computer: Hardware Requirements
|
||||
|
||||
### System
|
||||
### :octicons-cpu-24: System
|
||||
|
||||
You wil need one of the following:
|
||||
|
||||
- An NVIDIA-based graphics card with 4 GB or more VRAM memory.
|
||||
- An Apple computer with an M1 chip.
|
||||
- :simple-nvidia: An NVIDIA-based graphics card with 4 GB or more VRAM memory.
|
||||
- :fontawesome-brands-apple: An Apple computer with an M1 chip.
|
||||
|
||||
### Memory
|
||||
### :fontawesome-solid-memory: Memory
|
||||
|
||||
- At least 12 GB Main Memory RAM.
|
||||
|
||||
### Disk
|
||||
### :fontawesome-regular-hard-drive: Disk
|
||||
|
||||
- At least 6 GB of free disk space for the machine learning model, Python, and all its dependencies.
|
||||
|
||||
### Note
|
||||
!!! note
|
||||
|
||||
If you are have a Nvidia 10xx series card (e.g. the 1080ti), please run the dream script in
|
||||
full-precision mode as shown below.
|
||||
If you are have a Nvidia 10xx series card (e.g. the 1080ti), please run the dream script in
|
||||
full-precision mode as shown below.
|
||||
|
||||
Similarly, specify full-precision mode on Apple M1 hardware.
|
||||
Similarly, specify full-precision mode on Apple M1 hardware.
|
||||
|
||||
To run in full-precision mode, start `dream.py` with the `--full_precision` flag:
|
||||
To run in full-precision mode, start `dream.py` with the `--full_precision` flag:
|
||||
|
||||
```bash
|
||||
(ldm) ~/stable-diffusion$ python scripts/dream.py --full_precision
|
||||
```
|
||||
```bash
|
||||
(ldm) ~/stable-diffusion$ python scripts/dream.py --full_precision
|
||||
```
|
||||
## :octicons-log-16: Latest Changes
|
||||
|
||||
## Features
|
||||
### vNEXT <small>(TODO 2022)</small>
|
||||
|
||||
### Major Features
|
||||
|
||||
- [Interactive Command Line Interface](features/CLI.md)
|
||||
- [Image To Image](features/IMG2IMG.md)
|
||||
- [Inpainting Support](features/INPAINTING.md)
|
||||
- [GFPGAN and Real-ESRGAN Support](features/UPSCALE.md)
|
||||
- [Seamless Tiling](features/OTHER.md#seamless-tiling)
|
||||
- [Google Colab](features/OTHER.md#google-colab)
|
||||
- [Web Server](features/WEB.md)
|
||||
- [Reading Prompts From File](features/OTHER.md#reading-prompts-from-a-file)
|
||||
- [Shortcut: Reusing Seeds](features/OTHER.md#shortcuts-reusing-seeds)
|
||||
- [Weighted Prompts](features/OTHER.md#weighted-prompts)
|
||||
- [Variations](features/VARIATIONS.md)
|
||||
- [Personalizing Text-to-Image Generation](features/TEXTUAL_INVERSION.md)
|
||||
- [Simplified API for text to image generation](features/OTHER.md#simplified-api)
|
||||
|
||||
### Other Features
|
||||
|
||||
- [Creating Transparent Regions for Inpainting](features/INPAINTING.md#creating-transparent-regions-for-inpainting)
|
||||
- [Preload Models](features/OTHER.md#preload-models)
|
||||
|
||||
## Latest Changes
|
||||
- Deprecated `--full_precision` / `-F`. Simply omit it and `dream.py` will auto
|
||||
configure. To switch away from auto use the new flag like `--precision=float32`.
|
||||
|
||||
### v1.14 <small>(11 September 2022)</small>
|
||||
|
||||
@ -127,12 +131,12 @@ To run in full-precision mode, start `dream.py` with the `--full_precision` flag
|
||||
|
||||
For older changelogs, please visit the **[CHANGELOG](features/CHANGELOG.md)**.
|
||||
|
||||
## Troubleshooting
|
||||
## :material-target: Troubleshooting
|
||||
|
||||
Please check out our **[Q&A](help/TROUBLESHOOT.md)** to get solutions for common installation
|
||||
Please check out our **[:material-frequently-asked-questions: Q&A](help/TROUBLESHOOT.md)** to get solutions for common installation
|
||||
problems and other issues.
|
||||
|
||||
## Contributing
|
||||
## :octicons-repo-push-24: Contributing
|
||||
|
||||
Anyone who wishes to contribute to this project, whether documentation, features, bug fixes, code
|
||||
cleanup, testing, or code reviews, is very much encouraged to do so. If you are unfamiliar with how
|
||||
@ -144,13 +148,13 @@ important thing is to **make your pull request against the "development" branch*
|
||||
"main". This will help keep public breakage to a minimum and will allow you to propose more radical
|
||||
changes.
|
||||
|
||||
## Contributors
|
||||
## :octicons-person-24: Contributors
|
||||
|
||||
This fork is a combined effort of various people from across the world.
|
||||
[Check out the list of all these amazing people](other/CONTRIBUTORS.md). We thank them for their
|
||||
time, hard work and effort.
|
||||
|
||||
## Support
|
||||
## :octicons-question-24: Support
|
||||
|
||||
For support, please use this repository's GitHub Issues tracking service. Feel free to send me an
|
||||
email if you use and like the script.
|
||||
@ -158,7 +162,7 @@ email if you use and like the script.
|
||||
Original portions of the software are Copyright (c) 2020
|
||||
[Lincoln D. Stein](https://github.com/lstein)
|
||||
|
||||
## Further Reading
|
||||
## :octicons-book-24: Further Reading
|
||||
|
||||
Please see the original README for more information on this software and underlying algorithm,
|
||||
located in the file [README-CompViz.md](other/README-CompViz.md).
|
||||
|
183
docs/installation/INSTALL_DOCKER.md
Normal file
@ -0,0 +1,183 @@
|
||||
# Before you begin
|
||||
|
||||
- For end users: Install Stable Diffusion locally using the instructions for your OS.
|
||||
- For developers: For container-related development tasks or for enabling easy deployment to other environments (on-premises or cloud), follow these instructions. For general use, install locally to leverage your machine's GPU.
|
||||
|
||||
# Why containers?
|
||||
|
||||
They provide a flexible, reliable way to build and deploy Stable Diffusion. You'll also use a Docker volume to store the largest model files and image outputs as a first step in decoupling storage and compute. Future enhancements can do this for other assets. See [Processes](https://12factor.net/processes) under the Twelve-Factor App methodology for details on why running applications in such a stateless fashion is important.
|
||||
|
||||
You can specify the target platform when building the image and running the container. You'll also need to specify the Stable Diffusion requirements file that matches the container's OS and the architecture it will run on.
|
||||
|
||||
Developers on Apple silicon (M1/M2): You [can't access your GPU cores from Docker containers](https://github.com/pytorch/pytorch/issues/81224) and performance is reduced compared with running it directly on macOS but for development purposes it's fine. Once you're done with development tasks on your laptop you can build for the target platform and architecture and deploy to another environment with NVIDIA GPUs on-premises or in the cloud.
|
||||
|
||||
# Installation on a Linux container
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Get the data files
|
||||
|
||||
Go to [Hugging Face](https://huggingface.co/CompVis/stable-diffusion-v-1-4-original), and click "Access repository" to Download the model file ```sd-v1-4.ckpt``` (~4 GB) to ```~/Downloads```. You'll need to create an account but it's quick and free.
|
||||
|
||||
Also download the face restoration model.
|
||||
```Shell
|
||||
cd ~/Downloads
|
||||
wget https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.3.pth
|
||||
```
|
||||
|
||||
### Install [Docker](https://github.com/santisbon/guides#docker)
|
||||
On the Docker Desktop app, go to Preferences, Resources, Advanced. Increase the CPUs and Memory to avoid this [Issue](https://github.com/lstein/stable-diffusion/issues/342). You may need to increase Swap and Disk image size too.
|
||||
|
||||
## Setup
|
||||
|
||||
Set the fork you want to use and other variables.
|
||||
```Shell
|
||||
TAG_STABLE_DIFFUSION="santisbon/stable-diffusion"
|
||||
PLATFORM="linux/arm64"
|
||||
GITHUB_STABLE_DIFFUSION="-b orig-gfpgan https://github.com/santisbon/stable-diffusion.git"
|
||||
REQS_STABLE_DIFFUSION="requirements-linux-arm64.txt"
|
||||
CONDA_SUBDIR="osx-arm64"
|
||||
|
||||
echo $TAG_STABLE_DIFFUSION
|
||||
echo $PLATFORM
|
||||
echo $GITHUB_STABLE_DIFFUSION
|
||||
echo $REQS_STABLE_DIFFUSION
|
||||
echo $CONDA_SUBDIR
|
||||
```
|
||||
|
||||
Create a Docker volume for the downloaded model files.
|
||||
```Shell
|
||||
docker volume create my-vol
|
||||
```
|
||||
|
||||
Copy the data files to the Docker volume using a lightweight Linux container. We'll need the models at run time. You just need to create the container with the mountpoint; no need to run this dummy container.
|
||||
```Shell
|
||||
cd ~/Downloads # or wherever you saved the files
|
||||
|
||||
docker create --platform $PLATFORM --name dummy --mount source=my-vol,target=/data alpine
|
||||
|
||||
docker cp sd-v1-4.ckpt dummy:/data
|
||||
docker cp GFPGANv1.3.pth dummy:/data
|
||||
```
|
||||
|
||||
Get the repo and download the Miniconda installer (we'll need it at build time). Replace the URL with the version matching your container OS and the architecture it will run on.
|
||||
```Shell
|
||||
cd ~
|
||||
git clone $GITHUB_STABLE_DIFFUSION
|
||||
|
||||
cd stable-diffusion/docker-build
|
||||
chmod +x entrypoint.sh
|
||||
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-aarch64.sh -O anaconda.sh && chmod +x anaconda.sh
|
||||
```
|
||||
|
||||
Build the Docker image. Give it any tag ```-t``` that you want.
|
||||
Choose the Linux container's host platform: x86-64/Intel is ```amd64```. Apple silicon is ```arm64```. If deploying the container to the cloud to leverage powerful GPU instances you'll be on amd64 hardware but if you're just trying this out locally on Apple silicon choose arm64.
|
||||
The application uses libraries that need to match the host environment so use the appropriate requirements file.
|
||||
Tip: Check that your shell session has the env variables set above.
|
||||
```Shell
|
||||
docker build -t $TAG_STABLE_DIFFUSION \
|
||||
--platform $PLATFORM \
|
||||
--build-arg gsd=$GITHUB_STABLE_DIFFUSION \
|
||||
--build-arg rsd=$REQS_STABLE_DIFFUSION \
|
||||
--build-arg cs=$CONDA_SUBDIR \
|
||||
.
|
||||
```
|
||||
|
||||
Run a container using your built image.
|
||||
Tip: Make sure you've created and populated the Docker volume (above).
|
||||
```Shell
|
||||
docker run -it \
|
||||
--rm \
|
||||
--platform $PLATFORM \
|
||||
--name stable-diffusion \
|
||||
--hostname stable-diffusion \
|
||||
--mount source=my-vol,target=/data \
|
||||
$TAG_STABLE_DIFFUSION
|
||||
```
|
||||
|
||||
# Usage (time to have fun)
|
||||
|
||||
## Startup
|
||||
If you're on a **Linux container** the ```dream``` script is **automatically started** and the output dir set to the Docker volume you created earlier.
|
||||
|
||||
If you're **directly on macOS follow these startup instructions**.
|
||||
With the Conda environment activated (```conda activate ldm```), run the interactive interface that combines the functionality of the original scripts ```txt2img``` and ```img2img```:
|
||||
Use the more accurate but VRAM-intensive full precision math because half-precision requires autocast and won't work.
|
||||
By default the images are saved in ```outputs/img-samples/```.
|
||||
```Shell
|
||||
python3 scripts/dream.py --full_precision
|
||||
```
|
||||
|
||||
You'll get the script's prompt. You can see available options or quit.
|
||||
```Shell
|
||||
dream> -h
|
||||
dream> q
|
||||
```
|
||||
|
||||
## Text to Image
|
||||
For quick (but bad) image results test with 5 steps (default 50) and 1 sample image. This will let you know that everything is set up correctly.
|
||||
Then increase steps to 100 or more for good (but slower) results.
|
||||
The prompt can be in quotes or not.
|
||||
```Shell
|
||||
dream> The hulk fighting with sheldon cooper -s5 -n1
|
||||
dream> "woman closeup highly detailed" -s 150
|
||||
# Reuse previous seed and apply face restoration
|
||||
dream> "woman closeup highly detailed" --steps 150 --seed -1 -G 0.75
|
||||
```
|
||||
|
||||
You'll need to experiment to see if face restoration is making it better or worse for your specific prompt.
|
||||
|
||||
If you're on a container the output is set to the Docker volume. You can copy it wherever you want.
|
||||
You can download it from the Docker Desktop app, Volumes, my-vol, data.
|
||||
Or you can copy it from your Mac terminal. Keep in mind ```docker cp``` can't expand ```*.png``` so you'll need to specify the image file name.
|
||||
|
||||
On your host Mac (you can use the name of any container that mounted the volume):
|
||||
```Shell
|
||||
docker cp dummy:/data/000001.928403745.png /Users/<your-user>/Pictures
|
||||
```
|
||||
|
||||
## Image to Image
|
||||
You can also do text-guided image-to-image translation. For example, turning a sketch into a detailed drawing.
|
||||
|
||||
```strength``` is a value between 0.0 and 1.0 that controls the amount of noise that is added to the input image. Values that approach 1.0 allow for lots of variations but will also produce images that are not semantically consistent with the input. 0.0 preserves image exactly, 1.0 replaces it completely.
|
||||
|
||||
Make sure your input image size dimensions are multiples of 64 e.g. 512x512. Otherwise you'll get ```Error: product of dimension sizes > 2**31'```. If you still get the error [try a different size](https://support.apple.com/guide/preview/resize-rotate-or-flip-an-image-prvw2015/mac#:~:text=image's%20file%20size-,In%20the%20Preview%20app%20on%20your%20Mac%2C%20open%20the%20file,is%20shown%20at%20the%20bottom.) like 512x256.
|
||||
|
||||
If you're on a Docker container, copy your input image into the Docker volume
|
||||
```Shell
|
||||
docker cp /Users/<your-user>/Pictures/sketch-mountains-input.jpg dummy:/data/
|
||||
```
|
||||
|
||||
Try it out generating an image (or more). The ```dream``` script needs absolute paths to find the image so don't use ```~```.
|
||||
|
||||
If you're on your Mac
|
||||
```Shell
|
||||
dream> "A fantasy landscape, trending on artstation" -I /Users/<your-user>/Pictures/sketch-mountains-input.jpg --strength 0.75 --steps 100 -n4
|
||||
```
|
||||
If you're on a Linux container on your Mac
|
||||
```Shell
|
||||
dream> "A fantasy landscape, trending on artstation" -I /data/sketch-mountains-input.jpg --strength 0.75 --steps 50 -n1
|
||||
```
|
||||
|
||||
## Web Interface
|
||||
You can use the ```dream``` script with a graphical web interface. Start the web server with:
|
||||
```Shell
|
||||
python3 scripts/dream.py --full_precision --web
|
||||
```
|
||||
If it's running on your Mac point your Mac web browser to http://127.0.0.1:9090
|
||||
|
||||
Press Control-C at the command line to stop the web server.
|
||||
|
||||
## Notes
|
||||
|
||||
Some text you can add at the end of the prompt to make it very pretty:
|
||||
```Shell
|
||||
cinematic photo, highly detailed, cinematic lighting, ultra-detailed, ultrarealistic, photorealism, Octane Rendering, cyberpunk lights, Hyper Detail, 8K, HD, Unreal Engine, V-Ray, full hd, cyberpunk, abstract, 3d octane render + 4k UHD + immense detail + dramatic lighting + well lit + black, purple, blue, pink, cerulean, teal, metallic colours, + fine details, ultra photoreal, photographic, concept art, cinematic composition, rule of thirds, mysterious, eerie, photorealism, breathtaking detailed, painting art deco pattern, by hsiao, ron cheng, john james audubon, bizarre compositions, exquisite detail, extremely moody lighting, painted by greg rutkowski makoto shinkai takashi takeuchi studio ghibli, akihiko yoshida
|
||||
```
|
||||
|
||||
The original scripts should work as well.
|
||||
```Shell
|
||||
python3 scripts/orig_scripts/txt2img.py --help
|
||||
python3 scripts/orig_scripts/txt2img.py --ddim_steps 100 --n_iter 1 --n_samples 1 --plms --prompt "new born baby kitten. Hyper Detail, Octane Rendering, Unreal Engine, V-Ray"
|
||||
python3 scripts/orig_scripts/txt2img.py --ddim_steps 5 --n_iter 1 --n_samples 1 --plms --prompt "ocean" # or --klms
|
||||
```
|
@ -2,6 +2,10 @@
|
||||
title: Linux
|
||||
---
|
||||
|
||||
# :fontawesome-brands-linux: Linux
|
||||
|
||||
## Installation
|
||||
|
||||
1. You will need to install the following prerequisites if they are not already
|
||||
available. Use your operating system's preferred installer.
|
||||
|
||||
@ -55,9 +59,11 @@ title: Linux
|
||||
(ldm) ~/stable-diffusion$ python3 scripts/preload_models.py
|
||||
```
|
||||
|
||||
Note that this step is necessary because I modified the original just-in-time
|
||||
model loading scheme to allow the script to work on GPU machines that are not
|
||||
internet connected. See [Preload Models](../features/OTHER.md#preload-models)
|
||||
!!! note
|
||||
|
||||
This step is necessary because I modified the original just-in-time
|
||||
model loading scheme to allow the script to work on GPU machines that are not
|
||||
internet connected. See [Preload Models](../features/OTHER.md#preload-models)
|
||||
|
||||
7. Now you need to install the weights for the stable diffusion model.
|
||||
|
||||
@ -96,15 +102,15 @@ title: Linux
|
||||
launch the dream script (step 8). If you forget to activate the ldm
|
||||
environment, the script will fail with multiple `ModuleNotFound` errors.
|
||||
|
||||
### Updating to newer versions of the script
|
||||
## Updating to newer versions of the script
|
||||
|
||||
This distribution is changing rapidly. If you used the `git clone` method
|
||||
(step 5) to download the stable-diffusion directory, then to update to the
|
||||
latest and greatest version, launch the Anaconda window, enter
|
||||
`stable-diffusion` and type:
|
||||
This distribution is changing rapidly. If you used the `git clone` method
|
||||
(step 5) to download the stable-diffusion directory, then to update to the
|
||||
latest and greatest version, launch the Anaconda window, enter
|
||||
`stable-diffusion` and type:
|
||||
|
||||
```bash
|
||||
(ldm) ~/stable-diffusion$ git pull
|
||||
```
|
||||
```bash
|
||||
(ldm) ~/stable-diffusion$ git pull
|
||||
```
|
||||
|
||||
This will bring your local copy into sync with the remote one.
|
||||
This will bring your local copy into sync with the remote one.
|
||||
|
@ -2,6 +2,8 @@
|
||||
title: macOS
|
||||
---
|
||||
|
||||
# :fontawesome-brands-apple: macOS
|
||||
|
||||
## Requirements
|
||||
|
||||
- macOS 12.3 Monterey or later
|
||||
@ -9,18 +11,21 @@ title: macOS
|
||||
- Patience
|
||||
- Apple Silicon or Intel Mac
|
||||
|
||||
Things have moved really fast and so these instructions change often and are
|
||||
often out-of-date. One of the problems is that there are so many different ways
|
||||
to run this.
|
||||
Things have moved really fast and so these instructions change often which makes
|
||||
them outdated pretty fast. One of the problems is that there are so many
|
||||
different ways to run this.
|
||||
|
||||
We are trying to build a testing setup so that when we make changes it doesn't
|
||||
always break.
|
||||
|
||||
How to (this hasn't been 100% tested yet):
|
||||
## How to
|
||||
|
||||
First get the weights checkpoint download started - it's big:
|
||||
(this hasn't been 100% tested yet)
|
||||
|
||||
1. Sign up at https://huggingface.co
|
||||
First get the weights checkpoint download started since it's big and will take
|
||||
some time:
|
||||
|
||||
1. Sign up at [huggingface.co](https://huggingface.co)
|
||||
2. Go to the
|
||||
[Stable diffusion diffusion model page](https://huggingface.co/CompVis/stable-diffusion-v-1-4-original)
|
||||
3. Accept the terms and click Access Repository:
|
||||
@ -28,114 +33,147 @@ First get the weights checkpoint download started - it's big:
|
||||
[sd-v1-4.ckpt (4.27 GB)](https://huggingface.co/CompVis/stable-diffusion-v-1-4-original/blob/main/sd-v1-4.ckpt)
|
||||
and note where you have saved it (probably the Downloads folder)
|
||||
|
||||
While that is downloading, open Terminal and run the following commands one
|
||||
at a time.
|
||||
While that is downloading, open a Terminal and run the following commands:
|
||||
|
||||
```bash
|
||||
# install brew (and Xcode command line tools):
|
||||
!!! todo "Homebrew"
|
||||
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
=== "no brew installation yet"
|
||||
|
||||
# Now there are two different routes to get the Python (miniconda) environment up and running:
|
||||
# 1. Alongside pyenv
|
||||
# 2. No pyenv
|
||||
#
|
||||
# If you don't know what we are talking about, choose 2.
|
||||
#
|
||||
# NOW EITHER DO
|
||||
# 1. Installing alongside pyenv
|
||||
```bash title="install brew (and Xcode command line tools)"
|
||||
/bin/bash -c \
|
||||
"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
```
|
||||
|
||||
brew install pyenv-virtualenv # you might have this from before, no problem
|
||||
pyenv install anaconda3-2022.05
|
||||
pyenv virtualenv anaconda3-2022.05
|
||||
eval "$(pyenv init -)"
|
||||
pyenv activate anaconda3-2022.05
|
||||
=== "brew is already installed"
|
||||
|
||||
Only if you installed protobuf in a previous version of this tutorial, otherwise skip
|
||||
|
||||
# OR,
|
||||
# 2. Installing standalone
|
||||
# install python 3, git, cmake, protobuf:
|
||||
brew install cmake protobuf rust
|
||||
`#!bash brew uninstall protobuf`
|
||||
|
||||
# install miniconda for M1 arm64:
|
||||
curl https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-arm64.sh -o Miniconda3-latest-MacOSX-arm64.sh
|
||||
/bin/bash Miniconda3-latest-MacOSX-arm64.sh
|
||||
!!! todo "Conda Installation"
|
||||
|
||||
# OR install miniconda for Intel:
|
||||
curl https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.sh -o Miniconda3-latest-MacOSX-x86_64.sh
|
||||
/bin/bash Miniconda3-latest-MacOSX-x86_64.sh
|
||||
Now there are two different ways to set up the Python (miniconda) environment:
|
||||
1. Standalone
|
||||
2. with pyenv
|
||||
If you don't know what we are talking about, choose Standalone
|
||||
|
||||
=== "Standalone"
|
||||
|
||||
# EITHER WAY,
|
||||
# continue from here
|
||||
```bash
|
||||
# install cmake and rust:
|
||||
brew install cmake rust
|
||||
```
|
||||
|
||||
=== "M1 arm64"
|
||||
|
||||
```bash title="Install miniconda for M1 arm64"
|
||||
curl https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-arm64.sh \
|
||||
-o Miniconda3-latest-MacOSX-arm64.sh
|
||||
/bin/bash Miniconda3-latest-MacOSX-arm64.sh
|
||||
```
|
||||
|
||||
=== "Intel x86_64"
|
||||
|
||||
```bash title="Install miniconda for Intel"
|
||||
curl https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.sh \
|
||||
-o Miniconda3-latest-MacOSX-x86_64.sh
|
||||
/bin/bash Miniconda3-latest-MacOSX-x86_64.sh
|
||||
```
|
||||
|
||||
=== "with pyenv"
|
||||
|
||||
```{.bash .annotate}
|
||||
brew install rust pyenv-virtualenv # (1)!
|
||||
pyenv install anaconda3-2022.05
|
||||
pyenv virtualenv anaconda3-2022.05
|
||||
eval "$(pyenv init -)"
|
||||
pyenv activate anaconda3-2022.05
|
||||
```
|
||||
|
||||
1. You might already have this installed, if that is the case just continue.
|
||||
|
||||
```{.bash .annotate title="local repo setup"}
|
||||
# clone the repo
|
||||
git clone https://github.com/lstein/stable-diffusion.git
|
||||
cd stable-diffusion
|
||||
git clone https://github.com/lstein/stable-diffusion.git
|
||||
cd stable-diffusion
|
||||
|
||||
#
|
||||
# wait until the checkpoint file has downloaded, then proceed
|
||||
#
|
||||
|
||||
# create symlink to checkpoint
|
||||
mkdir -p models/ldm/stable-diffusion-v1/
|
||||
mkdir -p models/ldm/stable-diffusion-v1/
|
||||
|
||||
PATH_TO_CKPT="$HOME/Downloads" # or wherever you saved sd-v1-4.ckpt
|
||||
PATH_TO_CKPT="$HOME/Downloads" # (1)!
|
||||
|
||||
ln -s "$PATH_TO_CKPT/sd-v1-4.ckpt" models/ldm/stable-diffusion-v1/model.ckpt
|
||||
ln -s "$PATH_TO_CKPT/sd-v1-4.ckpt" \
|
||||
models/ldm/stable-diffusion-v1/model.ckpt
|
||||
```
|
||||
|
||||
# install packages for arm64
|
||||
PIP_EXISTS_ACTION=w CONDA_SUBDIR=osx-arm64 conda env create -f environment-mac.yaml
|
||||
conda activate ldm
|
||||
1. or wherever you saved sd-v1-4.ckpt
|
||||
|
||||
# OR install packages for x86_64
|
||||
PIP_EXISTS_ACTION=w CONDA_SUBDIR=osx-x86_64 conda env create -f environment-mac.yaml
|
||||
conda activate ldm
|
||||
!!! todo "create Conda Environment"
|
||||
|
||||
=== "M1 arm64"
|
||||
|
||||
```bash
|
||||
PIP_EXISTS_ACTION=w CONDA_SUBDIR=osx-arm64 \
|
||||
conda env create \
|
||||
-f environment-mac.yaml \
|
||||
&& conda activate ldm
|
||||
```
|
||||
|
||||
=== "Intel x86_64"
|
||||
|
||||
```bash
|
||||
PIP_EXISTS_ACTION=w CONDA_SUBDIR=osx-x86_64 \
|
||||
conda env create \
|
||||
-f environment-mac.yaml \
|
||||
&& conda activate ldm
|
||||
```
|
||||
|
||||
```{.bash .annotate title="preload models and run script"}
|
||||
# only need to do this once
|
||||
python scripts/preload_models.py
|
||||
|
||||
# run SD!
|
||||
python scripts/dream.py --full_precision # half-precision requires autocast and won't work
|
||||
# now you can run SD in CLI mode
|
||||
python scripts/dream.py --full_precision # (1)!
|
||||
|
||||
# or run the web interface!
|
||||
python scripts/dream.py --web
|
||||
|
||||
# The original scripts should work as well.
|
||||
python scripts/orig_scripts/txt2img.py \
|
||||
--prompt "a photograph of an astronaut riding a horse" \
|
||||
--plms
|
||||
```
|
||||
|
||||
The original scripts should work as well.
|
||||
1. half-precision requires autocast which is unfortunatelly incompatible
|
||||
|
||||
```bash
|
||||
python scripts/orig_scripts/txt2img.py --prompt "a photograph of an astronaut riding a horse" --plms
|
||||
```
|
||||
!!! note
|
||||
|
||||
Note,
|
||||
`#!bash export PIP_EXISTS_ACTION=w` is a precaution to fix a problem where
|
||||
|
||||
```bash
|
||||
export PIP_EXISTS_ACTION=w
|
||||
```
|
||||
```bash
|
||||
conda env create \
|
||||
-f environment-mac.yaml
|
||||
```
|
||||
|
||||
is a precaution to fix
|
||||
|
||||
```bash
|
||||
conda env create -f environment-mac.yaml
|
||||
```
|
||||
|
||||
never finishing in some situations. So it isn't required but wont hurt.
|
||||
|
||||
After you follow all the instructions and run dream.py you might get several
|
||||
errors. Here's the errors I've seen and found solutions for.
|
||||
did never finish in some situations. So it isn't required but wont hurt.
|
||||
|
||||
---
|
||||
|
||||
## Common problems
|
||||
|
||||
After you followed all the instructions and try to run dream.py, you might
|
||||
get several errors. Here's the errors I've seen and found solutions for.
|
||||
|
||||
### Is it slow?
|
||||
|
||||
Be sure to specify 1 sample and 1 iteration.
|
||||
|
||||
```bash
|
||||
```bash title="Be sure to specify 1 sample and 1 iteration."
|
||||
python ./scripts/orig_scripts/txt2img.py \
|
||||
--prompt "ocean" \
|
||||
--ddim_steps 5 \
|
||||
--n_samples 1 \
|
||||
--n_iter 1
|
||||
--prompt "ocean" \
|
||||
--ddim_steps 5 \
|
||||
--n_samples 1 \
|
||||
--n_iter 1
|
||||
```
|
||||
|
||||
---
|
||||
@ -153,55 +191,75 @@ solution please
|
||||
One debugging step is to update to the latest version of PyTorch nightly.
|
||||
|
||||
```bash
|
||||
conda install pytorch torchvision torchaudio -c pytorch-nightly
|
||||
conda install \
|
||||
pytorch \
|
||||
torchvision \
|
||||
-c pytorch-nightly \
|
||||
-n ldm
|
||||
```
|
||||
|
||||
If it takes forever to run
|
||||
|
||||
```bash
|
||||
conda env create -f environment-mac.yaml
|
||||
conda env create \
|
||||
-f environment-mac.yaml
|
||||
```
|
||||
|
||||
you could try to run `git clean -f` followed by:
|
||||
you could try to run:
|
||||
|
||||
`conda clean --yes --all`
|
||||
```bash
|
||||
git clean -f
|
||||
conda clean \
|
||||
--yes \
|
||||
--all
|
||||
```
|
||||
|
||||
Or you could try to completley reset Anaconda:
|
||||
|
||||
```bash
|
||||
conda update --force-reinstall -y -n base -c defaults conda
|
||||
conda update \
|
||||
--force-reinstall \
|
||||
-y \
|
||||
-n base \
|
||||
-c defaults conda
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### "No module named cv2", torch, 'ldm', 'transformers', 'taming', etc
|
||||
|
||||
There are several causes of these errors.
|
||||
There are several causes of these errors:
|
||||
|
||||
- First, did you remember to `conda activate ldm`? If your terminal prompt
|
||||
begins with "(ldm)" then you activated it. If it begins with "(base)" or
|
||||
something else you haven't.
|
||||
1. Did you remember to `conda activate ldm`? If your terminal prompt begins with
|
||||
"(ldm)" then you activated it. If it begins with "(base)" or something else
|
||||
you haven't.
|
||||
|
||||
- Second, you might've run `./scripts/preload_models.py` or `./scripts/dream.py`
|
||||
instead of `python ./scripts/preload_models.py` or
|
||||
`python ./scripts/dream.py`. The cause of this error is long so it's below.
|
||||
2. You might've run `./scripts/preload_models.py` or `./scripts/dream.py`
|
||||
instead of `python ./scripts/preload_models.py` or
|
||||
`python ./scripts/dream.py`. The cause of this error is long so it's below.
|
||||
|
||||
- Third, if it says you're missing taming you need to rebuild your virtual
|
||||
environment.
|
||||
<!-- I could not find out where the error is, otherwise would have marked it as a footnote -->
|
||||
|
||||
````bash
|
||||
conda deactivate
|
||||
3. if it says you're missing taming you need to rebuild your virtual
|
||||
environment.
|
||||
|
||||
conda env remove -n ldm
|
||||
PIP_EXISTS_ACTION=w CONDA_SUBDIR=osx-arm64 conda env create -f environment-mac.yaml
|
||||
```
|
||||
```bash
|
||||
conda deactivate
|
||||
conda env remove -n ldm
|
||||
PIP_EXISTS_ACTION=w CONDA_SUBDIR=osx-arm64 \
|
||||
conda env create \
|
||||
-f environment-mac.yaml
|
||||
```
|
||||
|
||||
Fourth, If you have activated the ldm virtual environment and tried rebuilding
|
||||
it, maybe the problem could be that I have something installed that you don't
|
||||
and you'll just need to manually install it. Make sure you activate the virtual
|
||||
environment so it installs there instead of globally.
|
||||
4. If you have activated the ldm virtual environment and tried rebuilding it,
|
||||
maybe the problem could be that I have something installed that you don't and
|
||||
you'll just need to manually install it. Make sure you activate the virtual
|
||||
environment so it installs there instead of globally.
|
||||
|
||||
`conda activate ldm pip install _name_`
|
||||
```bash
|
||||
conda activate ldm
|
||||
pip install <package name>
|
||||
```
|
||||
|
||||
You might also need to install Rust (I mention this again below).
|
||||
|
||||
@ -261,21 +319,20 @@ output of `python3 -V` and `python -V`.
|
||||
/Users/name/miniforge3/envs/ldm/bin/python
|
||||
```
|
||||
|
||||
The above is what you'll see if you have miniforge and you've correctly
|
||||
activated the ldm environment, and you used option 2 in the setup instructions
|
||||
above ("no pyenv").
|
||||
The above is what you'll see if you have miniforge and correctly activated the
|
||||
ldm environment, while usingd the standalone setup instructions above.
|
||||
|
||||
If you otherwise installed via pyenv, you will get this result:
|
||||
|
||||
```bash
|
||||
(anaconda3-2022.05) % which python
|
||||
/Users/name/.pyenv/shims/python
|
||||
```
|
||||
|
||||
... and the above is what you'll see if you used option 1 ("Alongside pyenv").
|
||||
|
||||
It's all a mess and you should know
|
||||
[how to modify the path environment variable](https://support.apple.com/guide/terminal/use-environment-variables-apd382cc5fa-4f58-4449-b20a-41c53c006f8f/mac)
|
||||
if you want to fix it. Here's a brief hint of all the ways you can modify it
|
||||
(don't really have the time to explain it all here).
|
||||
if you want to fix it. Here's a brief hint of the most common ways you can
|
||||
modify it (don't really have the time to explain it all here).
|
||||
|
||||
- ~/.zshrc
|
||||
- ~/.bash_profile
|
||||
@ -283,16 +340,21 @@ if you want to fix it. Here's a brief hint of all the ways you can modify it
|
||||
- /etc/paths.d
|
||||
- /etc/path
|
||||
|
||||
Which one you use will depend on what you have installed except putting a file
|
||||
in /etc/paths.d is what I prefer to do.
|
||||
Which one you use will depend on what you have installed, except putting a file
|
||||
in /etc/paths.d - which also is the way I prefer to do.
|
||||
|
||||
Finally, to answer the question posed by this section's title, it may help to
|
||||
list all of the `python` / `python3` things found in `$PATH` instead of just the
|
||||
one that will be executed by default. To do that, add the `-a` switch to
|
||||
`which`:
|
||||
first hit. To do so, add the `-a` switch to `which`:
|
||||
|
||||
% which -a python3
|
||||
...
|
||||
```bash
|
||||
% which -a python3
|
||||
...
|
||||
```
|
||||
|
||||
This will show a list of all binaries which are actually available in your PATH.
|
||||
|
||||
---
|
||||
|
||||
### Debugging?
|
||||
|
||||
@ -300,37 +362,56 @@ Tired of waiting for your renders to finish before you can see if it works?
|
||||
Reduce the steps! The image quality will be horrible but at least you'll get
|
||||
quick feedback.
|
||||
|
||||
python ./scripts/txt2img.py --prompt "ocean" --ddim_steps 5 --n_samples 1 --n_iter 1
|
||||
```bash
|
||||
python ./scripts/txt2img.py \
|
||||
--prompt "ocean" \
|
||||
--ddim_steps 5 \
|
||||
--n_samples 1 \
|
||||
--n_iter 1
|
||||
```
|
||||
|
||||
### OSError: Can't load tokenizer for 'openai/clip-vit-large-patch14'...
|
||||
---
|
||||
|
||||
python scripts/preload_models.py
|
||||
### OSError: Can't load tokenizer for 'openai/clip-vit-large-patch14'
|
||||
|
||||
```bash
|
||||
python scripts/preload_models.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### "The operator [name] is not current implemented for the MPS device." (sic)
|
||||
|
||||
Example error.
|
||||
!!! example "example error"
|
||||
|
||||
```
|
||||
```bash
|
||||
... NotImplementedError: The operator 'aten::_index_put_impl_' is not current
|
||||
implemented for the MPS device. If you want this op to be added in priority
|
||||
during the prototype phase of this feature, please comment on
|
||||
https://github.com/pytorch/pytorch/issues/77764.
|
||||
As a temporary fix, you can set the environment variable
|
||||
`PYTORCH_ENABLE_MPS_FALLBACK=1` to use the CPU as a fallback for this op.
|
||||
WARNING: this will be slower than running natively on MPS.
|
||||
```
|
||||
|
||||
... NotImplementedError: The operator 'aten::_index_put_impl_' is not current
|
||||
implemented for the MPS device. If you want this op to be added in priority
|
||||
during the prototype phase of this feature, please comment on
|
||||
[https://github.com/pytorch/pytorch/issues/77764](https://github.com/pytorch/pytorch/issues/77764).
|
||||
As a temporary fix, you can set the environment variable
|
||||
`PYTORCH_ENABLE_MPS_FALLBACK=1` to use the CPU as a fallback for this op.
|
||||
WARNING: this will be slower than running natively on MPS.
|
||||
|
||||
```
|
||||
|
||||
The lstein branch includes this fix in
|
||||
This fork already includes a fix for this in
|
||||
[environment-mac.yaml](https://github.com/lstein/stable-diffusion/blob/main/environment-mac.yaml).
|
||||
|
||||
---
|
||||
|
||||
### "Could not build wheels for tokenizers"
|
||||
|
||||
I have not seen this error because I had Rust installed on my computer before I
|
||||
started playing with Stable Diffusion. The fix is to install Rust.
|
||||
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
```bash
|
||||
curl \
|
||||
--proto '=https' \
|
||||
--tlsv1.2 \
|
||||
-sSf https://sh.rustup.rs | sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### How come `--seed` doesn't work?
|
||||
|
||||
@ -347,7 +428,9 @@ still working on it.
|
||||
|
||||
### libiomp5.dylib error?
|
||||
|
||||
OMP: Error #15: Initializing libiomp5.dylib, but found libomp.dylib already initialized.
|
||||
```bash
|
||||
OMP: Error #15: Initializing libiomp5.dylib, but found libomp.dylib already initialized.
|
||||
```
|
||||
|
||||
You are likely using an Intel package by mistake. Be sure to run conda with the
|
||||
environment variable `CONDA_SUBDIR=osx-arm64`, like so:
|
||||
@ -363,6 +446,8 @@ is a metapackage designed to prevent this, by making it impossible to install
|
||||
Do _not_ use `os.environ['KMP_DUPLICATE_LIB_OK']='True'` or equivalents as this
|
||||
masks the underlying issue of using Intel packages.
|
||||
|
||||
---
|
||||
|
||||
### Not enough memory
|
||||
|
||||
This seems to be a common problem and is probably the underlying problem for a
|
||||
@ -374,6 +459,8 @@ how that would affect the quality of the images though.
|
||||
|
||||
See [this issue](https://github.com/CompVis/stable-diffusion/issues/71).
|
||||
|
||||
---
|
||||
|
||||
### "Error: product of dimension sizes > 2\*\*31'"
|
||||
|
||||
This error happens with img2img, which I haven't played with too much yet. But I
|
||||
@ -388,6 +475,8 @@ BTW, 2\*\*31-1 =
|
||||
is also 32-bit signed [LONG_MAX](https://en.wikipedia.org/wiki/C_data_types) in
|
||||
C.
|
||||
|
||||
---
|
||||
|
||||
### I just got Rickrolled! Do I have a virus?
|
||||
|
||||
You don't have a virus. It's part of the project. Here's
|
||||
@ -400,6 +489,8 @@ call this "computer vision", sheesh).
|
||||
Actually, this could be happening because there's not enough RAM. You could try
|
||||
the `model.half()` suggestion or specify smaller output images.
|
||||
|
||||
---
|
||||
|
||||
### My images come out black
|
||||
|
||||
We might have this fixed, we are still testing.
|
||||
@ -428,7 +519,10 @@ This is a 32-bit vs 16-bit problem.
|
||||
### The processor must support the Intel bla bla bla
|
||||
|
||||
What? Intel? On an Apple Silicon?
|
||||
`bash Intel MKL FATAL ERROR: This system does not meet the minimum requirements for use of the Intel(R) Math Kernel Library. The processor must support the Intel(R) Supplemental Streaming SIMD Extensions 3 (Intel(R) SSSE3) instructions. The processor must support the Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) instructions. The processor must support the Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions. `
|
||||
|
||||
```bash
|
||||
Intel MKL FATAL ERROR: This system does not meet the minimum requirements for use of the Intel(R) Math Kernel Library. The processor must support the Intel(R) Supplemental Streaming SIMD Extensions 3 (Intel(R) SSSE3) instructions. The processor must support the Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) instructions. The processor must support the Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
|
||||
```
|
||||
|
||||
This is due to the Intel `mkl` package getting picked up when you try to install
|
||||
something that depends on it-- Rosetta can translate some Intel instructions but
|
||||
@ -453,5 +547,3 @@ Abort trap: 6
|
||||
warnings.warn('resource_tracker: There appear to be %d '
|
||||
```
|
||||
|
||||
Macs do not support `autocast/mixed-precision`, so you need to supply
|
||||
`--full_precision` to use float32 everywhere.
|
||||
|
@ -2,6 +2,8 @@
|
||||
title: Windows
|
||||
---
|
||||
|
||||
# :fontawesome-brands-windows: Windows
|
||||
|
||||
## **Notebook install (semi-automated)**
|
||||
|
||||
We have a
|
||||
@ -28,17 +30,16 @@ in the wiki
|
||||
|
||||
### **Conda**
|
||||
|
||||
1. Install Anaconda3 (miniconda3 version) from here:
|
||||
https://docs.anaconda.com/anaconda/install/windows/
|
||||
1. Install Anaconda3 (miniconda3 version) from [here](https://docs.anaconda.com/anaconda/install/windows/)
|
||||
|
||||
2. Install Git from here: https://git-scm.com/download/win
|
||||
2. Install Git from [here](https://git-scm.com/download/win)
|
||||
|
||||
3. Launch Anaconda from the Windows Start menu. This will bring up a command
|
||||
window. Type all the remaining commands in this window.
|
||||
|
||||
4. Run the command:
|
||||
|
||||
```bash
|
||||
```batch
|
||||
git clone https://github.com/lstein/stable-diffusion.git
|
||||
```
|
||||
|
||||
@ -48,15 +49,15 @@ in the wiki
|
||||
5. Enter the newly-created stable-diffusion folder. From this step forward make
|
||||
sure that you are working in the stable-diffusion directory!
|
||||
|
||||
```bash
|
||||
```batch
|
||||
cd stable-diffusion
|
||||
```
|
||||
|
||||
6. Run the following two commands:
|
||||
|
||||
```bash
|
||||
conda env create -f environment.yaml (step 6a)
|
||||
conda activate ldm (step 6b)
|
||||
```batch
|
||||
conda env create -f environment.yaml
|
||||
conda activate ldm
|
||||
```
|
||||
|
||||
This will install all python requirements and activate the "ldm" environment
|
||||
@ -64,7 +65,7 @@ in the wiki
|
||||
|
||||
7. Run the command:
|
||||
|
||||
```bash
|
||||
```batch
|
||||
python scripts\preload_models.py
|
||||
```
|
||||
|
||||
@ -77,9 +78,9 @@ in the wiki
|
||||
8. Now you need to install the weights for the big stable diffusion model.
|
||||
|
||||
- For running with the released weights, you will first need to set up an
|
||||
acount with Hugging Face (https://huggingface.co).
|
||||
acount with [Hugging Face](https://huggingface.co).
|
||||
- Use your credentials to log in, and then point your browser at
|
||||
https://huggingface.co/CompVis/stable-diffusion-v-1-4-original.
|
||||
[https://huggingface.co/CompVis/stable-diffusion-v-1-4-original](https://huggingface.co/CompVis/stable-diffusion-v-1-4-original).
|
||||
- You may be asked to sign a license agreement at this point.
|
||||
- Click on "Files and versions" near the top of the page, and then click on
|
||||
the file named `sd-v1-4.ckpt`. You'll be taken to a page that prompts you
|
||||
@ -90,7 +91,7 @@ in the wiki
|
||||
Now run the following commands from **within the stable-diffusion directory**
|
||||
to copy the weights file to the right place:
|
||||
|
||||
```bash
|
||||
```batch
|
||||
mkdir -p models\ldm\stable-diffusion-v1
|
||||
copy C:\path\to\sd-v1-4.ckpt models\ldm\stable-diffusion-v1\model.ckpt
|
||||
```
|
||||
@ -102,7 +103,7 @@ in the wiki
|
||||
|
||||
9. Start generating images!
|
||||
|
||||
```bash
|
||||
```batch
|
||||
# for the pre-release weights
|
||||
python scripts\dream.py -l
|
||||
|
||||
@ -122,14 +123,14 @@ in the wiki
|
||||
|
||||
---
|
||||
|
||||
### Updating to newer versions of the script
|
||||
## Updating to newer versions of the script
|
||||
|
||||
This distribution is changing rapidly. If you used the `git clone` method
|
||||
(step 5) to download the stable-diffusion directory, then to update to the
|
||||
latest and greatest version, launch the Anaconda window, enter
|
||||
`stable-diffusion`, and type:
|
||||
|
||||
```bash
|
||||
```batch
|
||||
git pull
|
||||
conda env update -f environment.yaml
|
||||
```
|
||||
|
@ -2,6 +2,8 @@
|
||||
title: Contributors
|
||||
---
|
||||
|
||||
# :octicons-person-24: Contributors
|
||||
|
||||
The list of all the amazing people who have contributed to the various features that you get to
|
||||
experience in this fork.
|
||||
|
||||
|
@ -32,6 +32,7 @@ dependencies:
|
||||
- omegaconf==2.1.1
|
||||
- onnx==1.12.0
|
||||
- onnxruntime==1.12.1
|
||||
- protobuf==3.20.1
|
||||
- pudb==2022.1
|
||||
- pytorch-lightning==1.6.5
|
||||
- scipy==1.9.1
|
||||
@ -48,6 +49,7 @@ dependencies:
|
||||
- opencv-python==4.6.0
|
||||
- protobuf==3.20.1
|
||||
- realesrgan==0.2.5.0
|
||||
- send2trash==1.8.0
|
||||
- test-tube==0.7.5
|
||||
- transformers==4.21.2
|
||||
- torch-fidelity==0.3.0
|
||||
|
@ -20,6 +20,7 @@ dependencies:
|
||||
- realesrgan==0.2.5.0
|
||||
- test-tube>=0.7.5
|
||||
- streamlit==1.12.0
|
||||
- send2trash==1.8.0
|
||||
- pillow==9.2.0
|
||||
- einops==0.3.0
|
||||
- torch-fidelity==0.3.0
|
||||
|
@ -1,85 +1,37 @@
|
||||
# Stable Diffusion Web UI
|
||||
|
||||
Demo at https://peaceful-otter-7a427f.netlify.app/ (not connected to back end)
|
||||
## Run
|
||||
|
||||
much of this readme is just notes for myself during dev work
|
||||
- `python backend/server.py` serves both frontend and backend at http://localhost:9090
|
||||
|
||||
numpy rand: 0 to 4294967295
|
||||
## Evironment
|
||||
|
||||
## Test and Build
|
||||
Install [node](https://nodejs.org/en/download/) (includes npm) and optionally
|
||||
[yarn](https://yarnpkg.com/getting-started/install).
|
||||
|
||||
from `frontend/`:
|
||||
From `frontend/` run `npm install` / `yarn install` to install the frontend packages.
|
||||
|
||||
- `yarn dev` runs `tsc-watch`, which runs `vite build` on successful `tsc` transpilation
|
||||
## Dev
|
||||
|
||||
from `.`:
|
||||
1. From `frontend/`, run `npm dev` / `yarn dev` to start the dev server.
|
||||
2. Note the address it starts up on (probably `http://localhost:5173/`).
|
||||
3. Edit `backend/server.py`'s `additional_allowed_origins` to include this address, e.g.
|
||||
`additional_allowed_origins = ['http://localhost:5173']`.
|
||||
4. Leaving the dev server running, open a new terminal and go to the project root.
|
||||
5. Run `python backend/server.py`.
|
||||
6. Navigate to the dev server address e.g. `http://localhost:5173/`.
|
||||
|
||||
- `python backend/server.py` serves both frontend and backend at http://localhost:9090
|
||||
To build for dev: `npm build-dev` / `yarn build-dev`
|
||||
|
||||
## API
|
||||
|
||||
`backend/server.py` serves the UI and provides a [socket.io](https://github.com/socketio/socket.io) API via [flask-socketio](https://github.com/miguelgrinberg/flask-socketio).
|
||||
|
||||
### Server Listeners
|
||||
|
||||
The server listens for these socket.io events:
|
||||
|
||||
`cancel`
|
||||
|
||||
- Cancels in-progress image generation
|
||||
- Returns ack only
|
||||
|
||||
`generateImage`
|
||||
|
||||
- Accepts object of image parameters
|
||||
- Generates an image
|
||||
- Returns ack only (image generation function sends progress and result via separate events)
|
||||
|
||||
`deleteImage`
|
||||
|
||||
- Accepts file path to image
|
||||
- Deletes image
|
||||
- Returns ack only
|
||||
|
||||
`deleteAllImages` WIP
|
||||
|
||||
- Deletes all images in `outputs/`
|
||||
- Returns ack only
|
||||
|
||||
`requestAllImages`
|
||||
|
||||
- Returns array of all images in `outputs/`
|
||||
|
||||
`requestCapabilities` WIP
|
||||
|
||||
- Returns capabilities of server (torch device, GFPGAN and ESRGAN availability, ???)
|
||||
|
||||
`sendImage` WIP
|
||||
|
||||
- Accepts a File and attributes
|
||||
- Saves image
|
||||
- Used to save init images which are not generated images
|
||||
|
||||
### Server Emitters
|
||||
|
||||
`progress`
|
||||
|
||||
- Emitted during each step in generation
|
||||
- Sends a number from 0 to 1 representing percentage of steps completed
|
||||
|
||||
`result` WIP
|
||||
|
||||
- Emitted when an image generation has completed
|
||||
- Sends a object:
|
||||
|
||||
```
|
||||
{
|
||||
url: relative_file_path,
|
||||
metadata: image_metadata_object
|
||||
}
|
||||
```
|
||||
To build for production: `npm build` / `yarn build`
|
||||
|
||||
## TODO
|
||||
|
||||
- Search repo for "TODO"
|
||||
- My one gripe with Chakra: no way to disable all animations right now and drop the dependence on `framer-motion`. I would prefer to save the ~30kb on bundle and have zero animations. This is on the Chakra roadmap. See https://github.com/chakra-ui/chakra-ui/pull/6368 for last discussion on this. Need to check in on this issue periodically.
|
||||
- Search repo for "TODO"
|
||||
- My one gripe with Chakra: no way to disable all animations right now and drop the dependence on
|
||||
`framer-motion`. I would prefer to save the ~30kb on bundle and have zero animations. This is on
|
||||
the Chakra roadmap. See https://github.com/chakra-ui/chakra-ui/pull/6368 for last discussion on
|
||||
this. Need to check in on this issue periodically.
|
||||
- Mobile friendly layout
|
||||
- Proper image gallery/viewer/manager
|
||||
- Help tooltips and such
|
||||
|
694
frontend/dist/assets/index.727a397b.js
vendored
Normal file
695
frontend/dist/assets/index.cc5cde43.js
vendored
2
frontend/dist/index.html
vendored
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Stable Diffusion Dream Server</title>
|
||||
<script type="module" crossorigin src="/assets/index.cc5cde43.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index.727a397b.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index.447eb2a9.css">
|
||||
</head>
|
||||
<body>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Stable Diffusion Dream Server</title>
|
||||
<title>InvokeAI Stable Diffusion Dream Server</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "sdui",
|
||||
"name": "invoke-ai-ui",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsc-watch --onSuccess 'yarn run vite build -m development'",
|
||||
"hmr": "vite dev",
|
||||
"dev": "vite dev",
|
||||
"build": "tsc && vite build",
|
||||
"build-dev": "tsc && vite build -m development",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/icons": "^2.0.10",
|
||||
"@chakra-ui/react": "^2.3.1",
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@emotion/styled": "^11.10.4",
|
||||
|
@ -1,60 +0,0 @@
|
||||
import { Grid, GridItem } from '@chakra-ui/react';
|
||||
import CurrentImage from './features/gallery/CurrentImage';
|
||||
import LogViewer from './features/system/LogViewer';
|
||||
import PromptInput from './features/sd/PromptInput';
|
||||
import ProgressBar from './features/header/ProgressBar';
|
||||
import { useEffect } from 'react';
|
||||
import { useAppDispatch } from './app/hooks';
|
||||
import { requestAllImages } from './app/socketio';
|
||||
import ProcessButtons from './features/sd/ProcessButtons';
|
||||
import ImageRoll from './features/gallery/ImageRoll';
|
||||
import SiteHeader from './features/header/SiteHeader';
|
||||
import OptionsAccordion from './features/sd/OptionsAccordion';
|
||||
|
||||
const App = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
useEffect(() => {
|
||||
dispatch(requestAllImages());
|
||||
}, [dispatch]);
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
width='100vw'
|
||||
height='100vh'
|
||||
templateAreas={`
|
||||
"header header header header"
|
||||
"progressBar progressBar progressBar progressBar"
|
||||
"menu prompt processButtons imageRoll"
|
||||
"menu currentImage currentImage imageRoll"`}
|
||||
gridTemplateRows={'36px 10px 100px auto'}
|
||||
gridTemplateColumns={'350px auto 100px 388px'}
|
||||
gap={2}
|
||||
>
|
||||
<GridItem area={'header'} pt={1}>
|
||||
<SiteHeader />
|
||||
</GridItem>
|
||||
<GridItem area={'progressBar'}>
|
||||
<ProgressBar />
|
||||
</GridItem>
|
||||
<GridItem pl='2' area={'menu'} overflowY='scroll'>
|
||||
<OptionsAccordion />
|
||||
</GridItem>
|
||||
<GridItem area={'prompt'}>
|
||||
<PromptInput />
|
||||
</GridItem>
|
||||
<GridItem area={'processButtons'}>
|
||||
<ProcessButtons />
|
||||
</GridItem>
|
||||
<GridItem area={'currentImage'}>
|
||||
<CurrentImage />
|
||||
</GridItem>
|
||||
<GridItem pr='2' area={'imageRoll'} overflowY='scroll'>
|
||||
<ImageRoll />
|
||||
</GridItem>
|
||||
</Grid>
|
||||
<LogViewer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
69
frontend/src/app/App.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { Grid, GridItem } from '@chakra-ui/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import CurrentImageDisplay from '../features/gallery/CurrentImageDisplay';
|
||||
import ImageGallery from '../features/gallery/ImageGallery';
|
||||
import ProgressBar from '../features/system/ProgressBar';
|
||||
import SiteHeader from '../features/system/SiteHeader';
|
||||
import OptionsAccordion from '../features/options/OptionsAccordion';
|
||||
import ProcessButtons from '../features/options/ProcessButtons';
|
||||
import PromptInput from '../features/options/PromptInput';
|
||||
import LogViewer from '../features/system/LogViewer';
|
||||
import Loading from '../Loading';
|
||||
import { useAppDispatch } from './store';
|
||||
import { requestAllImages, requestSystemConfig } from './socketio/actions';
|
||||
|
||||
const App = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const [isReady, setIsReady] = useState<boolean>(false);
|
||||
|
||||
// Load images from the gallery once
|
||||
useEffect(() => {
|
||||
dispatch(requestAllImages());
|
||||
dispatch(requestSystemConfig());
|
||||
setIsReady(true);
|
||||
}, [dispatch]);
|
||||
|
||||
return isReady ? (
|
||||
<>
|
||||
<Grid
|
||||
width="100vw"
|
||||
height="100vh"
|
||||
templateAreas={`
|
||||
"header header header header"
|
||||
"progressBar progressBar progressBar progressBar"
|
||||
"menu prompt processButtons imageRoll"
|
||||
"menu currentImage currentImage imageRoll"`}
|
||||
gridTemplateRows={'36px 10px 100px auto'}
|
||||
gridTemplateColumns={'350px auto 100px 388px'}
|
||||
gap={2}
|
||||
>
|
||||
<GridItem area={'header'} pt={1}>
|
||||
<SiteHeader />
|
||||
</GridItem>
|
||||
<GridItem area={'progressBar'}>
|
||||
<ProgressBar />
|
||||
</GridItem>
|
||||
<GridItem pl="2" area={'menu'} overflowY="scroll">
|
||||
<OptionsAccordion />
|
||||
</GridItem>
|
||||
<GridItem area={'prompt'}>
|
||||
<PromptInput />
|
||||
</GridItem>
|
||||
<GridItem area={'processButtons'}>
|
||||
<ProcessButtons />
|
||||
</GridItem>
|
||||
<GridItem area={'currentImage'}>
|
||||
<CurrentImageDisplay />
|
||||
</GridItem>
|
||||
<GridItem pr="2" area={'imageRoll'} overflowY="scroll">
|
||||
<ImageGallery />
|
||||
</GridItem>
|
||||
</Grid>
|
||||
<LogViewer />
|
||||
</>
|
||||
) : (
|
||||
<Loading />
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
@ -2,52 +2,52 @@
|
||||
|
||||
// Valid samplers
|
||||
export const SAMPLERS: Array<string> = [
|
||||
'ddim',
|
||||
'plms',
|
||||
'k_lms',
|
||||
'k_dpm_2',
|
||||
'k_dpm_2_a',
|
||||
'k_euler',
|
||||
'k_euler_a',
|
||||
'k_heun',
|
||||
'ddim',
|
||||
'plms',
|
||||
'k_lms',
|
||||
'k_dpm_2',
|
||||
'k_dpm_2_a',
|
||||
'k_euler',
|
||||
'k_euler_a',
|
||||
'k_heun',
|
||||
];
|
||||
|
||||
// Valid image widths
|
||||
export const WIDTHS: Array<number> = [
|
||||
64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960,
|
||||
1024,
|
||||
64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960,
|
||||
1024,
|
||||
];
|
||||
|
||||
// Valid image heights
|
||||
export const HEIGHTS: Array<number> = [
|
||||
64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960,
|
||||
1024,
|
||||
64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960,
|
||||
1024,
|
||||
];
|
||||
|
||||
// Valid upscaling levels
|
||||
export const UPSCALING_LEVELS: Array<{ key: string; value: number }> = [
|
||||
{ key: '2x', value: 2 },
|
||||
{ key: '4x', value: 4 },
|
||||
{ key: '2x', value: 2 },
|
||||
{ key: '4x', value: 4 },
|
||||
];
|
||||
|
||||
// Internal to human-readable parameters
|
||||
export const PARAMETERS: { [key: string]: string } = {
|
||||
prompt: 'Prompt',
|
||||
iterations: 'Iterations',
|
||||
steps: 'Steps',
|
||||
cfgScale: 'CFG Scale',
|
||||
height: 'Height',
|
||||
width: 'Width',
|
||||
sampler: 'Sampler',
|
||||
seed: 'Seed',
|
||||
img2imgStrength: 'img2img Strength',
|
||||
gfpganStrength: 'GFPGAN Strength',
|
||||
upscalingLevel: 'Upscaling Level',
|
||||
upscalingStrength: 'Upscaling Strength',
|
||||
initialImagePath: 'Initial Image',
|
||||
maskPath: 'Initial Image Mask',
|
||||
shouldFitToWidthHeight: 'Fit Initial Image',
|
||||
seamless: 'Seamless Tiling',
|
||||
prompt: 'Prompt',
|
||||
iterations: 'Iterations',
|
||||
steps: 'Steps',
|
||||
cfgScale: 'CFG Scale',
|
||||
height: 'Height',
|
||||
width: 'Width',
|
||||
sampler: 'Sampler',
|
||||
seed: 'Seed',
|
||||
img2imgStrength: 'img2img Strength',
|
||||
gfpganStrength: 'GFPGAN Strength',
|
||||
upscalingLevel: 'Upscaling Level',
|
||||
upscalingStrength: 'Upscaling Strength',
|
||||
initialImagePath: 'Initial Image',
|
||||
maskPath: 'Initial Image Mask',
|
||||
shouldFitToWidthHeight: 'Fit Initial Image',
|
||||
seamless: 'Seamless Tiling',
|
||||
};
|
||||
|
||||
export const NUMPY_RAND_MIN = 0;
|
||||
|
@ -1,7 +0,0 @@
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import type { TypedUseSelectorHook } from 'react-redux';
|
||||
import type { RootState, AppDispatch } from './store';
|
||||
|
||||
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch;
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
170
frontend/src/app/invokeai.d.ts
vendored
Normal file
@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Types for images, the things they are made of, and the things
|
||||
* they make up.
|
||||
*
|
||||
* Generated images are txt2img and img2img images. They may have
|
||||
* had additional postprocessing done on them when they were first
|
||||
* generated.
|
||||
*
|
||||
* Postprocessed images are images which were not generated here
|
||||
* but only postprocessed by the app. They only get postprocessing
|
||||
* metadata and have a different image type, e.g. 'esrgan' or
|
||||
* 'gfpgan'.
|
||||
*/
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* Once an image has been generated, if it is postprocessed again,
|
||||
* additional postprocessing steps are added to its postprocessing
|
||||
* array.
|
||||
*
|
||||
* TODO: Better documentation of types.
|
||||
*/
|
||||
|
||||
export declare type PromptItem = {
|
||||
prompt: string;
|
||||
weight: number;
|
||||
};
|
||||
|
||||
export declare type Prompt = Array<PromptItem>;
|
||||
|
||||
export declare type SeedWeightPair = {
|
||||
seed: number;
|
||||
weight: number;
|
||||
};
|
||||
|
||||
export declare type SeedWeights = Array<SeedWeightPair>;
|
||||
|
||||
// All generated images contain these metadata.
|
||||
export declare type CommonGeneratedImageMetadata = {
|
||||
postprocessing: null | Array<ESRGANMetadata | GFPGANMetadata>;
|
||||
sampler:
|
||||
| 'ddim'
|
||||
| 'k_dpm_2_a'
|
||||
| 'k_dpm_2'
|
||||
| 'k_euler_a'
|
||||
| 'k_euler'
|
||||
| 'k_heun'
|
||||
| 'k_lms'
|
||||
| 'plms';
|
||||
prompt: Prompt;
|
||||
seed: number;
|
||||
variations: SeedWeights;
|
||||
steps: number;
|
||||
cfg_scale: number;
|
||||
width: number;
|
||||
height: number;
|
||||
seamless: boolean;
|
||||
extra: null | Record<string, never>; // Pending development of RFC #266
|
||||
};
|
||||
|
||||
// txt2img and img2img images have some unique attributes.
|
||||
export declare type Txt2ImgMetadata = GeneratedImageMetadata & {
|
||||
type: 'txt2img';
|
||||
};
|
||||
|
||||
export declare type Img2ImgMetadata = GeneratedImageMetadata & {
|
||||
type: 'img2img';
|
||||
orig_hash: string;
|
||||
strength: number;
|
||||
fit: boolean;
|
||||
init_image_path: string;
|
||||
mask_image_path?: string;
|
||||
};
|
||||
|
||||
// Superset of generated image metadata types.
|
||||
export declare type GeneratedImageMetadata = Txt2ImgMetadata | Img2ImgMetadata;
|
||||
|
||||
// All post processed images contain these metadata.
|
||||
export declare type CommonPostProcessedImageMetadata = {
|
||||
orig_path: string;
|
||||
orig_hash: string;
|
||||
};
|
||||
|
||||
// esrgan and gfpgan images have some unique attributes.
|
||||
export declare type ESRGANMetadata = CommonPostProcessedImageMetadata & {
|
||||
type: 'esrgan';
|
||||
scale: 2 | 4;
|
||||
strength: number;
|
||||
};
|
||||
|
||||
export declare type GFPGANMetadata = CommonPostProcessedImageMetadata & {
|
||||
type: 'gfpgan';
|
||||
strength: number;
|
||||
};
|
||||
|
||||
// Superset of all postprocessed image metadata types..
|
||||
export declare type PostProcessedImageMetadata =
|
||||
| ESRGANMetadata
|
||||
| GFPGANMetadata;
|
||||
|
||||
// Metadata includes the system config and image metadata.
|
||||
export declare type Metadata = SystemConfig & {
|
||||
image: GeneratedImageMetadata | PostProcessedImageMetadata;
|
||||
};
|
||||
|
||||
// An Image has a UUID, url (path?) and Metadata.
|
||||
export declare type Image = {
|
||||
uuid: string;
|
||||
url: string;
|
||||
metadata: Metadata;
|
||||
};
|
||||
|
||||
// GalleryImages is an array of Image.
|
||||
export declare type GalleryImages = {
|
||||
images: Array<Image>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Types related to the system status.
|
||||
*/
|
||||
|
||||
// This represents the processing status of the backend.
|
||||
export declare type SystemStatus = {
|
||||
isProcessing: boolean;
|
||||
currentStep: number;
|
||||
totalSteps: number;
|
||||
currentIteration: number;
|
||||
totalIterations: number;
|
||||
currentStatus: string;
|
||||
currentStatusHasSteps: boolean;
|
||||
};
|
||||
|
||||
export declare type SystemConfig = {
|
||||
model: string;
|
||||
model_id: string;
|
||||
model_hash: string;
|
||||
app_id: string;
|
||||
app_version: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* These types type data received from the server via socketio.
|
||||
*/
|
||||
|
||||
export declare type SystemStatusResponse = SystemStatus;
|
||||
|
||||
export declare type SystemConfigResponse = SystemConfig;
|
||||
|
||||
export declare type ImageResultResponse = {
|
||||
url: string;
|
||||
metadata: Metadata;
|
||||
};
|
||||
|
||||
export declare type ErrorResponse = {
|
||||
message: string;
|
||||
additionalData?: string;
|
||||
};
|
||||
|
||||
export declare type GalleryImagesResponse = {
|
||||
images: Array<{ url: string; metadata: Metadata }>;
|
||||
};
|
||||
|
||||
export declare type ImageUrlAndUuidResponse = {
|
||||
uuid: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export declare type ImageUrlResponse = {
|
||||
url: string;
|
||||
};
|
@ -1,182 +0,0 @@
|
||||
import { SDState } from '../features/sd/sdSlice';
|
||||
import randomInt from '../features/sd/util/randomInt';
|
||||
import {
|
||||
seedWeightsToString,
|
||||
stringToSeedWeights,
|
||||
} from '../features/sd/util/seedWeightPairs';
|
||||
import { SystemState } from '../features/system/systemSlice';
|
||||
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from './constants';
|
||||
|
||||
/*
|
||||
These functions translate frontend state into parameters
|
||||
suitable for consumption by the backend, and vice-versa.
|
||||
*/
|
||||
|
||||
export const frontendToBackendParameters = (
|
||||
sdState: SDState,
|
||||
systemState: SystemState
|
||||
): { [key: string]: any } => {
|
||||
const {
|
||||
prompt,
|
||||
iterations,
|
||||
steps,
|
||||
cfgScale,
|
||||
height,
|
||||
width,
|
||||
sampler,
|
||||
seed,
|
||||
seamless,
|
||||
shouldUseInitImage,
|
||||
img2imgStrength,
|
||||
initialImagePath,
|
||||
maskPath,
|
||||
shouldFitToWidthHeight,
|
||||
shouldGenerateVariations,
|
||||
variantAmount,
|
||||
seedWeights,
|
||||
shouldRunESRGAN,
|
||||
upscalingLevel,
|
||||
upscalingStrength,
|
||||
shouldRunGFPGAN,
|
||||
gfpganStrength,
|
||||
shouldRandomizeSeed,
|
||||
} = sdState;
|
||||
|
||||
const { shouldDisplayInProgress } = systemState;
|
||||
|
||||
const generationParameters: { [k: string]: any } = {
|
||||
prompt,
|
||||
iterations,
|
||||
steps,
|
||||
cfg_scale: cfgScale,
|
||||
height,
|
||||
width,
|
||||
sampler_name: sampler,
|
||||
seed,
|
||||
seamless,
|
||||
progress_images: shouldDisplayInProgress,
|
||||
};
|
||||
|
||||
generationParameters.seed = shouldRandomizeSeed
|
||||
? randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX)
|
||||
: seed;
|
||||
|
||||
if (shouldUseInitImage) {
|
||||
generationParameters.init_img = initialImagePath;
|
||||
generationParameters.strength = img2imgStrength;
|
||||
generationParameters.fit = shouldFitToWidthHeight;
|
||||
if (maskPath) {
|
||||
generationParameters.init_mask = maskPath;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldGenerateVariations) {
|
||||
generationParameters.variation_amount = variantAmount;
|
||||
if (seedWeights) {
|
||||
generationParameters.with_variations =
|
||||
stringToSeedWeights(seedWeights);
|
||||
}
|
||||
} else {
|
||||
generationParameters.variation_amount = 0;
|
||||
}
|
||||
|
||||
let esrganParameters: false | { [k: string]: any } = false;
|
||||
let gfpganParameters: false | { [k: string]: any } = false;
|
||||
|
||||
if (shouldRunESRGAN) {
|
||||
esrganParameters = {
|
||||
level: upscalingLevel,
|
||||
strength: upscalingStrength,
|
||||
};
|
||||
}
|
||||
|
||||
if (shouldRunGFPGAN) {
|
||||
gfpganParameters = {
|
||||
strength: gfpganStrength,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
generationParameters,
|
||||
esrganParameters,
|
||||
gfpganParameters,
|
||||
};
|
||||
};
|
||||
|
||||
export const backendToFrontendParameters = (parameters: {
|
||||
[key: string]: any;
|
||||
}) => {
|
||||
const {
|
||||
prompt,
|
||||
iterations,
|
||||
steps,
|
||||
cfg_scale,
|
||||
height,
|
||||
width,
|
||||
sampler_name,
|
||||
seed,
|
||||
seamless,
|
||||
progress_images,
|
||||
variation_amount,
|
||||
with_variations,
|
||||
gfpgan_strength,
|
||||
upscale,
|
||||
init_img,
|
||||
init_mask,
|
||||
strength,
|
||||
} = parameters;
|
||||
|
||||
const sd: { [key: string]: any } = {
|
||||
shouldDisplayInProgress: progress_images,
|
||||
// init
|
||||
shouldGenerateVariations: false,
|
||||
shouldRunESRGAN: false,
|
||||
shouldRunGFPGAN: false,
|
||||
initialImagePath: '',
|
||||
maskPath: '',
|
||||
};
|
||||
|
||||
if (variation_amount > 0) {
|
||||
sd.shouldGenerateVariations = true;
|
||||
sd.variantAmount = variation_amount;
|
||||
if (with_variations) {
|
||||
sd.seedWeights = seedWeightsToString(with_variations);
|
||||
}
|
||||
}
|
||||
|
||||
if (gfpgan_strength > 0) {
|
||||
sd.shouldRunGFPGAN = true;
|
||||
sd.gfpganStrength = gfpgan_strength;
|
||||
}
|
||||
|
||||
if (upscale) {
|
||||
sd.shouldRunESRGAN = true;
|
||||
sd.upscalingLevel = upscale[0];
|
||||
sd.upscalingStrength = upscale[1];
|
||||
}
|
||||
|
||||
if (init_img) {
|
||||
sd.shouldUseInitImage = true
|
||||
sd.initialImagePath = init_img;
|
||||
sd.strength = strength;
|
||||
if (init_mask) {
|
||||
sd.maskPath = init_mask;
|
||||
}
|
||||
}
|
||||
|
||||
// if we had a prompt, add all the metadata, but if we don't have a prompt,
|
||||
// we must have only done ESRGAN or GFPGAN so do not add that metadata
|
||||
if (prompt) {
|
||||
sd.prompt = prompt;
|
||||
sd.iterations = iterations;
|
||||
sd.steps = steps;
|
||||
sd.cfgScale = cfg_scale;
|
||||
sd.height = height;
|
||||
sd.width = width;
|
||||
sd.sampler = sampler_name;
|
||||
sd.seed = seed;
|
||||
sd.seamless = seamless;
|
||||
}
|
||||
|
||||
return sd;
|
||||
};
|
@ -1,393 +0,0 @@
|
||||
import { createAction, Middleware } from '@reduxjs/toolkit';
|
||||
import { io } from 'socket.io-client';
|
||||
import {
|
||||
addImage,
|
||||
clearIntermediateImage,
|
||||
removeImage,
|
||||
SDImage,
|
||||
SDMetadata,
|
||||
setGalleryImages,
|
||||
setIntermediateImage,
|
||||
} from '../features/gallery/gallerySlice';
|
||||
import {
|
||||
addLogEntry,
|
||||
setCurrentStep,
|
||||
setIsConnected,
|
||||
setIsProcessing,
|
||||
} from '../features/system/systemSlice';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { setInitialImagePath, setMaskPath } from '../features/sd/sdSlice';
|
||||
import {
|
||||
backendToFrontendParameters,
|
||||
frontendToBackendParameters,
|
||||
} from './parameterTranslation';
|
||||
|
||||
export interface SocketIOResponse {
|
||||
status: 'OK' | 'ERROR';
|
||||
message?: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
export const socketioMiddleware = () => {
|
||||
const { hostname, port } = new URL(window.location.href);
|
||||
|
||||
const socketio = io(`http://${hostname}:9090`);
|
||||
|
||||
let areListenersSet = false;
|
||||
|
||||
const middleware: Middleware = (store) => (next) => (action) => {
|
||||
const { dispatch, getState } = store;
|
||||
if (!areListenersSet) {
|
||||
// CONNECT
|
||||
socketio.on('connect', () => {
|
||||
try {
|
||||
dispatch(setIsConnected(true));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
// DISCONNECT
|
||||
socketio.on('disconnect', () => {
|
||||
try {
|
||||
dispatch(setIsConnected(false));
|
||||
dispatch(setIsProcessing(false));
|
||||
dispatch(addLogEntry(`Disconnected from server`));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
// PROCESSING RESULT
|
||||
socketio.on(
|
||||
'result',
|
||||
(data: {
|
||||
url: string;
|
||||
type: 'generation' | 'esrgan' | 'gfpgan';
|
||||
uuid?: string;
|
||||
metadata: { [key: string]: any };
|
||||
}) => {
|
||||
try {
|
||||
const newUuid = uuidv4();
|
||||
const { type, url, uuid, metadata } = data;
|
||||
switch (type) {
|
||||
case 'generation': {
|
||||
const translatedMetadata =
|
||||
backendToFrontendParameters(metadata);
|
||||
dispatch(
|
||||
addImage({
|
||||
uuid: newUuid,
|
||||
url,
|
||||
metadata: translatedMetadata,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
addLogEntry(`Image generated: ${url}`)
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'esrgan': {
|
||||
const originalImage =
|
||||
getState().gallery.images.find(
|
||||
(i: SDImage) => i.uuid === uuid
|
||||
);
|
||||
const newMetadata = {
|
||||
...originalImage.metadata,
|
||||
};
|
||||
newMetadata.shouldRunESRGAN = true;
|
||||
newMetadata.upscalingLevel =
|
||||
metadata.upscale[0];
|
||||
newMetadata.upscalingStrength =
|
||||
metadata.upscale[1];
|
||||
dispatch(
|
||||
addImage({
|
||||
uuid: newUuid,
|
||||
url,
|
||||
metadata: newMetadata,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
addLogEntry(`ESRGAN upscaled: ${url}`)
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'gfpgan': {
|
||||
const originalImage =
|
||||
getState().gallery.images.find(
|
||||
(i: SDImage) => i.uuid === uuid
|
||||
);
|
||||
const newMetadata = {
|
||||
...originalImage.metadata,
|
||||
};
|
||||
newMetadata.shouldRunGFPGAN = true;
|
||||
newMetadata.gfpganStrength =
|
||||
metadata.gfpgan_strength;
|
||||
dispatch(
|
||||
addImage({
|
||||
uuid: newUuid,
|
||||
url,
|
||||
metadata: newMetadata,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
addLogEntry(`GFPGAN fixed faces: ${url}`)
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
dispatch(setIsProcessing(false));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// PROGRESS UPDATE
|
||||
socketio.on('progress', (data: { step: number }) => {
|
||||
try {
|
||||
dispatch(setIsProcessing(true));
|
||||
dispatch(setCurrentStep(data.step));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
// INTERMEDIATE IMAGE
|
||||
socketio.on(
|
||||
'intermediateResult',
|
||||
(data: { url: string; metadata: SDMetadata }) => {
|
||||
try {
|
||||
const uuid = uuidv4();
|
||||
const { url, metadata } = data;
|
||||
dispatch(
|
||||
setIntermediateImage({
|
||||
uuid,
|
||||
url,
|
||||
metadata,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
addLogEntry(`Intermediate image generated: ${url}`)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ERROR FROM BACKEND
|
||||
socketio.on('error', (message) => {
|
||||
try {
|
||||
dispatch(addLogEntry(`Server error: ${message}`));
|
||||
dispatch(setIsProcessing(false));
|
||||
dispatch(clearIntermediateImage());
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
areListenersSet = true;
|
||||
}
|
||||
|
||||
// HANDLE ACTIONS
|
||||
|
||||
switch (action.type) {
|
||||
// GENERATE IMAGE
|
||||
case 'socketio/generateImage': {
|
||||
dispatch(setIsProcessing(true));
|
||||
dispatch(setCurrentStep(-1));
|
||||
|
||||
const {
|
||||
generationParameters,
|
||||
esrganParameters,
|
||||
gfpganParameters,
|
||||
} = frontendToBackendParameters(
|
||||
getState().sd,
|
||||
getState().system
|
||||
);
|
||||
|
||||
socketio.emit(
|
||||
'generateImage',
|
||||
generationParameters,
|
||||
esrganParameters,
|
||||
gfpganParameters
|
||||
);
|
||||
|
||||
dispatch(
|
||||
addLogEntry(
|
||||
`Image generation requested: ${JSON.stringify({
|
||||
...generationParameters,
|
||||
...esrganParameters,
|
||||
...gfpganParameters,
|
||||
})}`
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// RUN ESRGAN (UPSCALING)
|
||||
case 'socketio/runESRGAN': {
|
||||
const imageToProcess = action.payload;
|
||||
dispatch(setIsProcessing(true));
|
||||
dispatch(setCurrentStep(-1));
|
||||
const { upscalingLevel, upscalingStrength } = getState().sd;
|
||||
const esrganParameters = {
|
||||
upscale: [upscalingLevel, upscalingStrength],
|
||||
};
|
||||
socketio.emit('runESRGAN', imageToProcess, esrganParameters);
|
||||
dispatch(
|
||||
addLogEntry(
|
||||
`ESRGAN upscale requested: ${JSON.stringify({
|
||||
file: imageToProcess.url,
|
||||
...esrganParameters,
|
||||
})}`
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// RUN GFPGAN (FIX FACES)
|
||||
case 'socketio/runGFPGAN': {
|
||||
const imageToProcess = action.payload;
|
||||
dispatch(setIsProcessing(true));
|
||||
dispatch(setCurrentStep(-1));
|
||||
const { gfpganStrength } = getState().sd;
|
||||
|
||||
const gfpganParameters = {
|
||||
gfpgan_strength: gfpganStrength,
|
||||
};
|
||||
socketio.emit('runGFPGAN', imageToProcess, gfpganParameters);
|
||||
dispatch(
|
||||
addLogEntry(
|
||||
`GFPGAN fix faces requested: ${JSON.stringify({
|
||||
file: imageToProcess.url,
|
||||
...gfpganParameters,
|
||||
})}`
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// DELETE IMAGE
|
||||
case 'socketio/deleteImage': {
|
||||
const imageToDelete = action.payload;
|
||||
const { url } = imageToDelete;
|
||||
socketio.emit(
|
||||
'deleteImage',
|
||||
url,
|
||||
(response: SocketIOResponse) => {
|
||||
if (response.status === 'OK') {
|
||||
dispatch(removeImage(imageToDelete));
|
||||
dispatch(addLogEntry(`Image deleted: ${url}`));
|
||||
}
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// GET ALL IMAGES FOR GALLERY
|
||||
case 'socketio/requestAllImages': {
|
||||
socketio.emit(
|
||||
'requestAllImages',
|
||||
(response: SocketIOResponse) => {
|
||||
dispatch(setGalleryImages(response.data));
|
||||
dispatch(
|
||||
addLogEntry(`Loaded ${response.data.length} images`)
|
||||
);
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// CANCEL PROCESSING
|
||||
case 'socketio/cancelProcessing': {
|
||||
socketio.emit('cancel', (response: SocketIOResponse) => {
|
||||
const { intermediateImage } = getState().gallery;
|
||||
if (response.status === 'OK') {
|
||||
dispatch(setIsProcessing(false));
|
||||
if (intermediateImage) {
|
||||
dispatch(addImage(intermediateImage));
|
||||
dispatch(
|
||||
addLogEntry(
|
||||
`Intermediate image saved: ${intermediateImage.url}`
|
||||
)
|
||||
);
|
||||
|
||||
dispatch(clearIntermediateImage());
|
||||
}
|
||||
dispatch(addLogEntry(`Processing canceled`));
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// UPLOAD INITIAL IMAGE
|
||||
case 'socketio/uploadInitialImage': {
|
||||
const file = action.payload;
|
||||
|
||||
socketio.emit(
|
||||
'uploadInitialImage',
|
||||
file,
|
||||
file.name,
|
||||
(response: SocketIOResponse) => {
|
||||
if (response.status === 'OK') {
|
||||
dispatch(setInitialImagePath(response.data));
|
||||
dispatch(
|
||||
addLogEntry(
|
||||
`Initial image uploaded: ${response.data}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// UPLOAD MASK IMAGE
|
||||
case 'socketio/uploadMaskImage': {
|
||||
const file = action.payload;
|
||||
|
||||
socketio.emit(
|
||||
'uploadMaskImage',
|
||||
file,
|
||||
file.name,
|
||||
(response: SocketIOResponse) => {
|
||||
if (response.status === 'OK') {
|
||||
dispatch(setMaskPath(response.data));
|
||||
dispatch(
|
||||
addLogEntry(
|
||||
`Mask image uploaded: ${response.data}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
next(action);
|
||||
};
|
||||
|
||||
return middleware;
|
||||
};
|
||||
|
||||
// Actions to be used by app
|
||||
|
||||
export const generateImage = createAction<undefined>('socketio/generateImage');
|
||||
export const runESRGAN = createAction<SDImage>('socketio/runESRGAN');
|
||||
export const runGFPGAN = createAction<SDImage>('socketio/runGFPGAN');
|
||||
export const deleteImage = createAction<SDImage>('socketio/deleteImage');
|
||||
export const requestAllImages = createAction<undefined>(
|
||||
'socketio/requestAllImages'
|
||||
);
|
||||
export const cancelProcessing = createAction<undefined>(
|
||||
'socketio/cancelProcessing'
|
||||
);
|
||||
export const uploadInitialImage = createAction<File>(
|
||||
'socketio/uploadInitialImage'
|
||||
);
|
||||
export const uploadMaskImage = createAction<File>('socketio/uploadMaskImage');
|
26
frontend/src/app/socketio/actions.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import * as InvokeAI from '../invokeai';
|
||||
|
||||
/**
|
||||
* We can't use redux-toolkit's createSlice() to make these actions,
|
||||
* because they have no associated reducer. They only exist to dispatch
|
||||
* requests to the server via socketio. These actions will be handled
|
||||
* by the middleware.
|
||||
*/
|
||||
|
||||
export const generateImage = createAction<undefined>('socketio/generateImage');
|
||||
export const runESRGAN = createAction<InvokeAI.Image>('socketio/runESRGAN');
|
||||
export const runGFPGAN = createAction<InvokeAI.Image>('socketio/runGFPGAN');
|
||||
export const deleteImage = createAction<InvokeAI.Image>('socketio/deleteImage');
|
||||
export const requestAllImages = createAction<undefined>(
|
||||
'socketio/requestAllImages'
|
||||
);
|
||||
export const cancelProcessing = createAction<undefined>(
|
||||
'socketio/cancelProcessing'
|
||||
);
|
||||
export const uploadInitialImage = createAction<File>(
|
||||
'socketio/uploadInitialImage'
|
||||
);
|
||||
export const uploadMaskImage = createAction<File>('socketio/uploadMaskImage');
|
||||
|
||||
export const requestSystemConfig = createAction<undefined>('socketio/requestSystemConfig');
|
104
frontend/src/app/socketio/emitters.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { AnyAction, Dispatch, MiddlewareAPI } from '@reduxjs/toolkit';
|
||||
import dateFormat from 'dateformat';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { frontendToBackendParameters } from '../../common/util/parameterTranslation';
|
||||
import {
|
||||
addLogEntry,
|
||||
setIsProcessing,
|
||||
} from '../../features/system/systemSlice';
|
||||
import * as InvokeAI from '../invokeai';
|
||||
|
||||
/**
|
||||
* Returns an object containing all functions which use `socketio.emit()`.
|
||||
* i.e. those which make server requests.
|
||||
*/
|
||||
const makeSocketIOEmitters = (
|
||||
store: MiddlewareAPI<Dispatch<AnyAction>, any>,
|
||||
socketio: Socket
|
||||
) => {
|
||||
// We need to dispatch actions to redux and get pieces of state from the store.
|
||||
const { dispatch, getState } = store;
|
||||
|
||||
return {
|
||||
emitGenerateImage: () => {
|
||||
dispatch(setIsProcessing(true));
|
||||
|
||||
const { generationParameters, esrganParameters, gfpganParameters } =
|
||||
frontendToBackendParameters(getState().options, getState().system);
|
||||
|
||||
socketio.emit(
|
||||
'generateImage',
|
||||
generationParameters,
|
||||
esrganParameters,
|
||||
gfpganParameters
|
||||
);
|
||||
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Image generation requested: ${JSON.stringify({
|
||||
...generationParameters,
|
||||
...esrganParameters,
|
||||
...gfpganParameters,
|
||||
})}`,
|
||||
})
|
||||
);
|
||||
},
|
||||
emitRunESRGAN: (imageToProcess: InvokeAI.Image) => {
|
||||
dispatch(setIsProcessing(true));
|
||||
const { upscalingLevel, upscalingStrength } = getState().options;
|
||||
const esrganParameters = {
|
||||
upscale: [upscalingLevel, upscalingStrength],
|
||||
};
|
||||
socketio.emit('runESRGAN', imageToProcess, esrganParameters);
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `ESRGAN upscale requested: ${JSON.stringify({
|
||||
file: imageToProcess.url,
|
||||
...esrganParameters,
|
||||
})}`,
|
||||
})
|
||||
);
|
||||
},
|
||||
emitRunGFPGAN: (imageToProcess: InvokeAI.Image) => {
|
||||
dispatch(setIsProcessing(true));
|
||||
const { gfpganStrength } = getState().options;
|
||||
|
||||
const gfpganParameters = {
|
||||
gfpgan_strength: gfpganStrength,
|
||||
};
|
||||
socketio.emit('runGFPGAN', imageToProcess, gfpganParameters);
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `GFPGAN fix faces requested: ${JSON.stringify({
|
||||
file: imageToProcess.url,
|
||||
...gfpganParameters,
|
||||
})}`,
|
||||
})
|
||||
);
|
||||
},
|
||||
emitDeleteImage: (imageToDelete: InvokeAI.Image) => {
|
||||
const { url, uuid } = imageToDelete;
|
||||
socketio.emit('deleteImage', url, uuid);
|
||||
},
|
||||
emitRequestAllImages: () => {
|
||||
socketio.emit('requestAllImages');
|
||||
},
|
||||
emitCancelProcessing: () => {
|
||||
socketio.emit('cancel');
|
||||
},
|
||||
emitUploadInitialImage: (file: File) => {
|
||||
socketio.emit('uploadInitialImage', file, file.name);
|
||||
},
|
||||
emitUploadMaskImage: (file: File) => {
|
||||
socketio.emit('uploadMaskImage', file, file.name);
|
||||
},
|
||||
emitRequestSystemConfig: () => {
|
||||
socketio.emit('requestSystemConfig')
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default makeSocketIOEmitters;
|
300
frontend/src/app/socketio/listeners.ts
Normal file
@ -0,0 +1,300 @@
|
||||
import { AnyAction, MiddlewareAPI, Dispatch } from '@reduxjs/toolkit';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import dateFormat from 'dateformat';
|
||||
|
||||
import * as InvokeAI from '../invokeai';
|
||||
|
||||
import {
|
||||
addLogEntry,
|
||||
setIsConnected,
|
||||
setIsProcessing,
|
||||
setSystemStatus,
|
||||
setCurrentStatus,
|
||||
setSystemConfig,
|
||||
} from '../../features/system/systemSlice';
|
||||
|
||||
import {
|
||||
addImage,
|
||||
clearIntermediateImage,
|
||||
removeImage,
|
||||
setGalleryImages,
|
||||
setIntermediateImage,
|
||||
} from '../../features/gallery/gallerySlice';
|
||||
|
||||
import {
|
||||
setInitialImagePath,
|
||||
setMaskPath,
|
||||
} from '../../features/options/optionsSlice';
|
||||
|
||||
/**
|
||||
* Returns an object containing listener callbacks for socketio events.
|
||||
* TODO: This file is large, but simple. Should it be split up further?
|
||||
*/
|
||||
const makeSocketIOListeners = (
|
||||
store: MiddlewareAPI<Dispatch<AnyAction>, any>
|
||||
) => {
|
||||
const { dispatch, getState } = store;
|
||||
|
||||
return {
|
||||
/**
|
||||
* Callback to run when we receive a 'connect' event.
|
||||
*/
|
||||
onConnect: () => {
|
||||
try {
|
||||
dispatch(setIsConnected(true));
|
||||
dispatch(setCurrentStatus('Connected'));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Callback to run when we receive a 'disconnect' event.
|
||||
*/
|
||||
onDisconnect: () => {
|
||||
try {
|
||||
dispatch(setIsConnected(false));
|
||||
dispatch(setIsProcessing(false));
|
||||
dispatch(setCurrentStatus('Disconnected'));
|
||||
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Disconnected from server`,
|
||||
level: 'warning',
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Callback to run when we receive a 'generationResult' event.
|
||||
*/
|
||||
onGenerationResult: (data: InvokeAI.ImageResultResponse) => {
|
||||
try {
|
||||
const { url, metadata } = data;
|
||||
const newUuid = uuidv4();
|
||||
|
||||
dispatch(
|
||||
addImage({
|
||||
uuid: newUuid,
|
||||
url,
|
||||
metadata: metadata,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Image generated: ${url}`,
|
||||
})
|
||||
);
|
||||
dispatch(setIsProcessing(false));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Callback to run when we receive a 'intermediateResult' event.
|
||||
*/
|
||||
onIntermediateResult: (data: InvokeAI.ImageResultResponse) => {
|
||||
try {
|
||||
const uuid = uuidv4();
|
||||
const { url, metadata } = data;
|
||||
dispatch(
|
||||
setIntermediateImage({
|
||||
uuid,
|
||||
url,
|
||||
metadata,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Intermediate image generated: ${url}`,
|
||||
})
|
||||
);
|
||||
dispatch(setIsProcessing(false));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Callback to run when we receive an 'esrganResult' event.
|
||||
*/
|
||||
onESRGANResult: (data: InvokeAI.ImageResultResponse) => {
|
||||
try {
|
||||
const { url, metadata } = data;
|
||||
|
||||
dispatch(
|
||||
addImage({
|
||||
uuid: uuidv4(),
|
||||
url,
|
||||
metadata,
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Upscaled: ${url}`,
|
||||
})
|
||||
);
|
||||
dispatch(setIsProcessing(false));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Callback to run when we receive a 'gfpganResult' event.
|
||||
*/
|
||||
onGFPGANResult: (data: InvokeAI.ImageResultResponse) => {
|
||||
try {
|
||||
const { url, metadata } = data;
|
||||
|
||||
dispatch(
|
||||
addImage({
|
||||
uuid: uuidv4(),
|
||||
url,
|
||||
metadata,
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Fixed faces: ${url}`,
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Callback to run when we receive a 'progressUpdate' event.
|
||||
* TODO: Add additional progress phases
|
||||
*/
|
||||
onProgressUpdate: (data: InvokeAI.SystemStatus) => {
|
||||
try {
|
||||
dispatch(setIsProcessing(true));
|
||||
dispatch(setSystemStatus(data));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Callback to run when we receive a 'progressUpdate' event.
|
||||
*/
|
||||
onError: (data: InvokeAI.ErrorResponse) => {
|
||||
const { message, additionalData } = data;
|
||||
|
||||
if (additionalData) {
|
||||
// TODO: handle more data than short message
|
||||
}
|
||||
|
||||
try {
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Server error: ${message}`,
|
||||
level: 'error',
|
||||
})
|
||||
);
|
||||
dispatch(setIsProcessing(false));
|
||||
dispatch(clearIntermediateImage());
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Callback to run when we receive a 'galleryImages' event.
|
||||
*/
|
||||
onGalleryImages: (data: InvokeAI.GalleryImagesResponse) => {
|
||||
const { images } = data;
|
||||
const preparedImages = images.map((image): InvokeAI.Image => {
|
||||
const { url, metadata } = image;
|
||||
return {
|
||||
uuid: uuidv4(),
|
||||
url,
|
||||
metadata,
|
||||
};
|
||||
});
|
||||
dispatch(setGalleryImages(preparedImages));
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Loaded ${images.length} images`,
|
||||
})
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Callback to run when we receive a 'processingCanceled' event.
|
||||
*/
|
||||
onProcessingCanceled: () => {
|
||||
dispatch(setIsProcessing(false));
|
||||
|
||||
const { intermediateImage } = getState().gallery;
|
||||
|
||||
if (intermediateImage) {
|
||||
dispatch(addImage(intermediateImage));
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Intermediate image saved: ${intermediateImage.url}`,
|
||||
})
|
||||
);
|
||||
dispatch(clearIntermediateImage());
|
||||
}
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Processing canceled`,
|
||||
level: 'warning',
|
||||
})
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Callback to run when we receive a 'imageDeleted' event.
|
||||
*/
|
||||
onImageDeleted: (data: InvokeAI.ImageUrlAndUuidResponse) => {
|
||||
const { url, uuid } = data;
|
||||
dispatch(removeImage(uuid));
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Image deleted: ${url}`,
|
||||
})
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Callback to run when we receive a 'initialImageUploaded' event.
|
||||
*/
|
||||
onInitialImageUploaded: (data: InvokeAI.ImageUrlResponse) => {
|
||||
const { url } = data;
|
||||
dispatch(setInitialImagePath(url));
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Initial image uploaded: ${url}`,
|
||||
})
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Callback to run when we receive a 'maskImageUploaded' event.
|
||||
*/
|
||||
onMaskImageUploaded: (data: InvokeAI.ImageUrlResponse) => {
|
||||
const { url } = data;
|
||||
dispatch(setMaskPath(url));
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Mask image uploaded: ${url}`,
|
||||
})
|
||||
);
|
||||
},
|
||||
onSystemConfig: (data: InvokeAI.SystemConfig) => {
|
||||
dispatch(setSystemConfig(data));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default makeSocketIOListeners;
|
173
frontend/src/app/socketio/middleware.ts
Normal file
@ -0,0 +1,173 @@
|
||||
import { Middleware } from '@reduxjs/toolkit';
|
||||
import { io } from 'socket.io-client';
|
||||
|
||||
import makeSocketIOListeners from './listeners';
|
||||
import makeSocketIOEmitters from './emitters';
|
||||
|
||||
import * as InvokeAI from '../invokeai';
|
||||
|
||||
/**
|
||||
* Creates a socketio middleware to handle communication with server.
|
||||
*
|
||||
* Special `socketio/actionName` actions are created in actions.ts and
|
||||
* exported for use by the application, which treats them like any old
|
||||
* action, using `dispatch` to dispatch them.
|
||||
*
|
||||
* These actions are intercepted here, where `socketio.emit()` calls are
|
||||
* made on their behalf - see `emitters.ts`. The emitter functions
|
||||
* are the outbound communication to the server.
|
||||
*
|
||||
* Listeners are also established here - see `listeners.ts`. The listener
|
||||
* functions receive communication from the server and usually dispatch
|
||||
* some new action to handle whatever data was sent from the server.
|
||||
*/
|
||||
export const socketioMiddleware = () => {
|
||||
const { hostname, port } = new URL(window.location.href);
|
||||
|
||||
const socketio = io(`http://${hostname}:9090`);
|
||||
|
||||
let areListenersSet = false;
|
||||
|
||||
const middleware: Middleware = (store) => (next) => (action) => {
|
||||
const {
|
||||
onConnect,
|
||||
onDisconnect,
|
||||
onError,
|
||||
onESRGANResult,
|
||||
onGFPGANResult,
|
||||
onGenerationResult,
|
||||
onIntermediateResult,
|
||||
onProgressUpdate,
|
||||
onGalleryImages,
|
||||
onProcessingCanceled,
|
||||
onImageDeleted,
|
||||
onInitialImageUploaded,
|
||||
onMaskImageUploaded,
|
||||
onSystemConfig,
|
||||
} = makeSocketIOListeners(store);
|
||||
|
||||
const {
|
||||
emitGenerateImage,
|
||||
emitRunESRGAN,
|
||||
emitRunGFPGAN,
|
||||
emitDeleteImage,
|
||||
emitRequestAllImages,
|
||||
emitCancelProcessing,
|
||||
emitUploadInitialImage,
|
||||
emitUploadMaskImage,
|
||||
emitRequestSystemConfig,
|
||||
} = makeSocketIOEmitters(store, socketio);
|
||||
|
||||
/**
|
||||
* If this is the first time the middleware has been called (e.g. during store setup),
|
||||
* initialize all our socket.io listeners.
|
||||
*/
|
||||
if (!areListenersSet) {
|
||||
socketio.on('connect', () => onConnect());
|
||||
|
||||
socketio.on('disconnect', () => onDisconnect());
|
||||
|
||||
socketio.on('error', (data: InvokeAI.ErrorResponse) => onError(data));
|
||||
|
||||
socketio.on('generationResult', (data: InvokeAI.ImageResultResponse) =>
|
||||
onGenerationResult(data)
|
||||
);
|
||||
|
||||
socketio.on('esrganResult', (data: InvokeAI.ImageResultResponse) =>
|
||||
onESRGANResult(data)
|
||||
);
|
||||
|
||||
socketio.on('gfpganResult', (data: InvokeAI.ImageResultResponse) =>
|
||||
onGFPGANResult(data)
|
||||
);
|
||||
|
||||
socketio.on('intermediateResult', (data: InvokeAI.ImageResultResponse) =>
|
||||
onIntermediateResult(data)
|
||||
);
|
||||
|
||||
socketio.on('progressUpdate', (data: InvokeAI.SystemStatus) =>
|
||||
onProgressUpdate(data)
|
||||
);
|
||||
|
||||
socketio.on('galleryImages', (data: InvokeAI.GalleryImagesResponse) =>
|
||||
onGalleryImages(data)
|
||||
);
|
||||
|
||||
socketio.on('processingCanceled', () => {
|
||||
onProcessingCanceled();
|
||||
});
|
||||
|
||||
socketio.on('imageDeleted', (data: InvokeAI.ImageUrlAndUuidResponse) => {
|
||||
onImageDeleted(data);
|
||||
});
|
||||
|
||||
socketio.on('initialImageUploaded', (data: InvokeAI.ImageUrlResponse) => {
|
||||
onInitialImageUploaded(data);
|
||||
});
|
||||
|
||||
socketio.on('maskImageUploaded', (data: InvokeAI.ImageUrlResponse) => {
|
||||
onMaskImageUploaded(data);
|
||||
});
|
||||
|
||||
socketio.on('systemConfig', (data: InvokeAI.SystemConfig) => {
|
||||
onSystemConfig(data);
|
||||
});
|
||||
|
||||
areListenersSet = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle redux actions caught by middleware.
|
||||
*/
|
||||
switch (action.type) {
|
||||
case 'socketio/generateImage': {
|
||||
emitGenerateImage();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'socketio/runESRGAN': {
|
||||
emitRunESRGAN(action.payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'socketio/runGFPGAN': {
|
||||
emitRunGFPGAN(action.payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'socketio/deleteImage': {
|
||||
emitDeleteImage(action.payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'socketio/requestAllImages': {
|
||||
emitRequestAllImages();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'socketio/cancelProcessing': {
|
||||
emitCancelProcessing();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'socketio/uploadInitialImage': {
|
||||
emitUploadInitialImage(action.payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'socketio/uploadMaskImage': {
|
||||
emitUploadMaskImage(action.payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'socketio/requestSystemConfig': {
|
||||
emitRequestSystemConfig();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
next(action);
|
||||
};
|
||||
|
||||
return middleware;
|
||||
};
|
@ -1,53 +1,78 @@
|
||||
import { combineReducers, configureStore } from '@reduxjs/toolkit';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import type { TypedUseSelectorHook } from 'react-redux';
|
||||
|
||||
import { persistReducer } from 'redux-persist';
|
||||
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web
|
||||
|
||||
import sdReducer from '../features/sd/sdSlice';
|
||||
import optionsReducer from '../features/options/optionsSlice';
|
||||
import galleryReducer from '../features/gallery/gallerySlice';
|
||||
import systemReducer from '../features/system/systemSlice';
|
||||
import { socketioMiddleware } from './socketio';
|
||||
import { socketioMiddleware } from './socketio/middleware';
|
||||
|
||||
const reducers = combineReducers({
|
||||
sd: sdReducer,
|
||||
gallery: galleryReducer,
|
||||
system: systemReducer,
|
||||
});
|
||||
/**
|
||||
* redux-persist provides an easy and reliable way to persist state across reloads.
|
||||
*
|
||||
* While we definitely want generation parameters to be persisted, there are a number
|
||||
* of things we do *not* want to be persisted across reloads:
|
||||
* - Gallery/selected image (user may add/delete images from disk between page loads)
|
||||
* - Connection/processing status
|
||||
* - Availability of external libraries like ESRGAN/GFPGAN
|
||||
*
|
||||
* These can be blacklisted in redux-persist.
|
||||
*
|
||||
* The necesssary nested persistors with blacklists are configured below.
|
||||
*
|
||||
* TODO: Do we blacklist initialImagePath? If the image is deleted from disk we get an
|
||||
* ugly 404. But if we blacklist it, then this is a valuable parameter that is lost
|
||||
* on reload. Need to figure out a good way to handle this.
|
||||
*/
|
||||
|
||||
const persistConfig = {
|
||||
const rootPersistConfig = {
|
||||
key: 'root',
|
||||
storage,
|
||||
blacklist: ['gallery', 'system'],
|
||||
};
|
||||
|
||||
const persistedReducer = persistReducer(persistConfig, reducers);
|
||||
const systemPersistConfig = {
|
||||
key: 'system',
|
||||
storage,
|
||||
blacklist: [
|
||||
'isConnected',
|
||||
'isProcessing',
|
||||
'currentStep',
|
||||
'socketId',
|
||||
'isESRGANAvailable',
|
||||
'isGFPGANAvailable',
|
||||
'currentStep',
|
||||
'totalSteps',
|
||||
'currentIteration',
|
||||
'totalIterations',
|
||||
'currentStatus',
|
||||
],
|
||||
};
|
||||
|
||||
/*
|
||||
The frontend needs to be distributed as a production build, so
|
||||
we cannot reasonably ask users to edit the JS and specify the
|
||||
host and port on which the socket.io server will run.
|
||||
|
||||
The solution is to allow server script to be run with arguments
|
||||
(or just edited) providing the host and port. Then, the server
|
||||
serves a route `/socketio_config` which responds with the host
|
||||
and port.
|
||||
|
||||
When the frontend loads, it synchronously requests that route
|
||||
and thus gets the host and port. This requires a suspicious
|
||||
fetch somewhere, and the store setup seems like as good a place
|
||||
as any to make this fetch request.
|
||||
*/
|
||||
const reducers = combineReducers({
|
||||
options: optionsReducer,
|
||||
gallery: galleryReducer,
|
||||
system: persistReducer(systemPersistConfig, systemReducer),
|
||||
});
|
||||
|
||||
const persistedReducer = persistReducer(rootPersistConfig, reducers);
|
||||
|
||||
// Continue with store setup
|
||||
export const store = configureStore({
|
||||
reducer: persistedReducer,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({
|
||||
// redux-persist sometimes needs to have a function in redux, need to disable this check
|
||||
// redux-persist sometimes needs to temporarily put a function in redux state, need to disable this check
|
||||
serializableCheck: false,
|
||||
}).concat(socketioMiddleware()),
|
||||
});
|
||||
|
||||
// Infer the `RootState` and `AppDispatch` types from the store itself
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
|
||||
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch;
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
|
@ -33,5 +33,20 @@ export const theme = extendTheme({
|
||||
fontWeight: 'light',
|
||||
},
|
||||
},
|
||||
Button: {
|
||||
variants: {
|
||||
imageHoverIconButton: (props: StyleFunctionProps) => ({
|
||||
bg: props.colorMode === 'dark' ? 'blackAlpha.700' : 'whiteAlpha.800',
|
||||
color:
|
||||
props.colorMode === 'dark' ? 'whiteAlpha.700' : 'blackAlpha.700',
|
||||
_hover: {
|
||||
bg:
|
||||
props.colorMode === 'dark' ? 'blackAlpha.800' : 'whiteAlpha.800',
|
||||
color:
|
||||
props.colorMode === 'dark' ? 'whiteAlpha.900' : 'blackAlpha.900',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
21
frontend/src/common/components/SDButton.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { Button, ButtonProps } from '@chakra-ui/react';
|
||||
|
||||
interface Props extends ButtonProps {
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable customized button component. Originally was more customized - now probably unecessary.
|
||||
*
|
||||
* TODO: Get rid of this.
|
||||
*/
|
||||
const SDButton = (props: Props) => {
|
||||
const { label, size = 'sm', ...rest } = props;
|
||||
return (
|
||||
<Button size={size} {...rest}>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default SDButton;
|
@ -16,6 +16,9 @@ interface Props extends NumberInputProps {
|
||||
width?: string | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customized Chakra FormControl + NumberInput multi-part component.
|
||||
*/
|
||||
const SDNumberInput = (props: Props) => {
|
||||
const {
|
||||
label,
|
||||
@ -31,7 +34,7 @@ const SDNumberInput = (props: Props) => {
|
||||
<Flex gap={2} justifyContent={'space-between'} alignItems={'center'}>
|
||||
{label && (
|
||||
<FormLabel marginBottom={1}>
|
||||
<Text fontSize={fontSize} whiteSpace='nowrap'>
|
||||
<Text fontSize={fontSize} whiteSpace="nowrap">
|
||||
{label}
|
||||
</Text>
|
||||
</FormLabel>
|
||||
@ -42,7 +45,7 @@ const SDNumberInput = (props: Props) => {
|
||||
keepWithinRange={false}
|
||||
clampValueOnBlur={true}
|
||||
>
|
||||
<NumberInputField fontSize={'md'}/>
|
||||
<NumberInputField fontSize={'md'} />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
56
frontend/src/common/components/SDSelect.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import {
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Select,
|
||||
SelectProps,
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
interface Props extends SelectProps {
|
||||
label: string;
|
||||
validValues:
|
||||
| Array<number | string>
|
||||
| Array<{ key: string; value: string | number }>;
|
||||
}
|
||||
/**
|
||||
* Customized Chakra FormControl + Select multi-part component.
|
||||
*/
|
||||
const SDSelect = (props: Props) => {
|
||||
const {
|
||||
label,
|
||||
isDisabled,
|
||||
validValues,
|
||||
size = 'sm',
|
||||
fontSize = 'md',
|
||||
marginBottom = 1,
|
||||
whiteSpace = 'nowrap',
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<FormControl isDisabled={isDisabled}>
|
||||
<Flex justifyContent={'space-between'} alignItems={'center'}>
|
||||
<FormLabel marginBottom={marginBottom}>
|
||||
<Text fontSize={fontSize} whiteSpace={whiteSpace}>
|
||||
{label}
|
||||
</Text>
|
||||
</FormLabel>
|
||||
<Select fontSize={fontSize} size={size} {...rest}>
|
||||
{validValues.map((opt) => {
|
||||
return typeof opt === 'string' || typeof opt === 'number' ? (
|
||||
<option key={opt} value={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
) : (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.key}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default SDSelect;
|
@ -11,6 +11,9 @@ interface Props extends SwitchProps {
|
||||
width?: string | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customized Chakra FormControl + Switch multi-part component.
|
||||
*/
|
||||
const SDSwitch = (props: Props) => {
|
||||
const {
|
||||
label,
|
||||
@ -28,7 +31,7 @@ const SDSwitch = (props: Props) => {
|
||||
fontSize={fontSize}
|
||||
marginBottom={1}
|
||||
flexGrow={2}
|
||||
whiteSpace='nowrap'
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
104
frontend/src/common/hooks/useCheckParameters.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import { OptionsState } from '../../features/options/optionsSlice';
|
||||
import { SystemState } from '../../features/system/systemSlice';
|
||||
import { validateSeedWeights } from '../util/seedWeightPairs';
|
||||
|
||||
const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
return {
|
||||
prompt: options.prompt,
|
||||
shouldGenerateVariations: options.shouldGenerateVariations,
|
||||
seedWeights: options.seedWeights,
|
||||
maskPath: options.maskPath,
|
||||
initialImagePath: options.initialImagePath,
|
||||
seed: options.seed,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isProcessing: system.isProcessing,
|
||||
isConnected: system.isConnected,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Checks relevant pieces of state to confirm generation will not deterministically fail.
|
||||
* This is used to prevent the 'Generate' button from being clicked.
|
||||
*/
|
||||
const useCheckParameters = (): boolean => {
|
||||
const {
|
||||
prompt,
|
||||
shouldGenerateVariations,
|
||||
seedWeights,
|
||||
maskPath,
|
||||
initialImagePath,
|
||||
seed,
|
||||
} = useAppSelector(optionsSelector);
|
||||
|
||||
const { isProcessing, isConnected } = useAppSelector(systemSelector);
|
||||
|
||||
return useMemo(() => {
|
||||
// Cannot generate without a prompt
|
||||
if (!prompt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot generate with a mask without img2img
|
||||
if (maskPath && !initialImagePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: job queue
|
||||
// Cannot generate if already processing an image
|
||||
if (isProcessing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot generate if not connected
|
||||
if (!isConnected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot generate variations without valid seed weights
|
||||
if (
|
||||
shouldGenerateVariations &&
|
||||
(!(validateSeedWeights(seedWeights) || seedWeights === '') || seed === -1)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// All good
|
||||
return true;
|
||||
}, [
|
||||
prompt,
|
||||
maskPath,
|
||||
initialImagePath,
|
||||
isProcessing,
|
||||
isConnected,
|
||||
shouldGenerateVariations,
|
||||
seedWeights,
|
||||
seed,
|
||||
]);
|
||||
};
|
||||
|
||||
export default useCheckParameters;
|
182
frontend/src/common/util/parameterTranslation.ts
Normal file
@ -0,0 +1,182 @@
|
||||
/*
|
||||
These functions translate frontend state into parameters
|
||||
suitable for consumption by the backend, and vice-versa.
|
||||
*/
|
||||
|
||||
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from '../../app/constants';
|
||||
import { OptionsState } from '../../features/options/optionsSlice';
|
||||
import { SystemState } from '../../features/system/systemSlice';
|
||||
import {
|
||||
seedWeightsToString,
|
||||
stringToSeedWeightsArray,
|
||||
} from './seedWeightPairs';
|
||||
import randomInt from './randomInt';
|
||||
|
||||
export const frontendToBackendParameters = (
|
||||
optionsState: OptionsState,
|
||||
systemState: SystemState
|
||||
): { [key: string]: any } => {
|
||||
const {
|
||||
prompt,
|
||||
iterations,
|
||||
steps,
|
||||
cfgScale,
|
||||
height,
|
||||
width,
|
||||
sampler,
|
||||
seed,
|
||||
seamless,
|
||||
shouldUseInitImage,
|
||||
img2imgStrength,
|
||||
initialImagePath,
|
||||
maskPath,
|
||||
shouldFitToWidthHeight,
|
||||
shouldGenerateVariations,
|
||||
variationAmount,
|
||||
seedWeights,
|
||||
shouldRunESRGAN,
|
||||
upscalingLevel,
|
||||
upscalingStrength,
|
||||
shouldRunGFPGAN,
|
||||
gfpganStrength,
|
||||
shouldRandomizeSeed,
|
||||
} = optionsState;
|
||||
|
||||
const { shouldDisplayInProgress } = systemState;
|
||||
|
||||
const generationParameters: { [k: string]: any } = {
|
||||
prompt,
|
||||
iterations,
|
||||
steps,
|
||||
cfg_scale: cfgScale,
|
||||
height,
|
||||
width,
|
||||
sampler_name: sampler,
|
||||
seed,
|
||||
seamless,
|
||||
progress_images: shouldDisplayInProgress,
|
||||
};
|
||||
|
||||
generationParameters.seed = shouldRandomizeSeed
|
||||
? randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX)
|
||||
: seed;
|
||||
|
||||
if (shouldUseInitImage) {
|
||||
generationParameters.init_img = initialImagePath;
|
||||
generationParameters.strength = img2imgStrength;
|
||||
generationParameters.fit = shouldFitToWidthHeight;
|
||||
if (maskPath) {
|
||||
generationParameters.init_mask = maskPath;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldGenerateVariations) {
|
||||
generationParameters.variation_amount = variationAmount;
|
||||
if (seedWeights) {
|
||||
generationParameters.with_variations =
|
||||
stringToSeedWeightsArray(seedWeights);
|
||||
}
|
||||
} else {
|
||||
generationParameters.variation_amount = 0;
|
||||
}
|
||||
|
||||
let esrganParameters: false | { [k: string]: any } = false;
|
||||
let gfpganParameters: false | { [k: string]: any } = false;
|
||||
|
||||
if (shouldRunESRGAN) {
|
||||
esrganParameters = {
|
||||
level: upscalingLevel,
|
||||
strength: upscalingStrength,
|
||||
};
|
||||
}
|
||||
|
||||
if (shouldRunGFPGAN) {
|
||||
gfpganParameters = {
|
||||
strength: gfpganStrength,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
generationParameters,
|
||||
esrganParameters,
|
||||
gfpganParameters,
|
||||
};
|
||||
};
|
||||
|
||||
export const backendToFrontendParameters = (parameters: {
|
||||
[key: string]: any;
|
||||
}) => {
|
||||
const {
|
||||
prompt,
|
||||
iterations,
|
||||
steps,
|
||||
cfg_scale,
|
||||
height,
|
||||
width,
|
||||
sampler_name,
|
||||
seed,
|
||||
seamless,
|
||||
progress_images,
|
||||
variation_amount,
|
||||
with_variations,
|
||||
gfpgan_strength,
|
||||
upscale,
|
||||
init_img,
|
||||
init_mask,
|
||||
strength,
|
||||
} = parameters;
|
||||
|
||||
const options: { [key: string]: any } = {
|
||||
shouldDisplayInProgress: progress_images,
|
||||
// init
|
||||
shouldGenerateVariations: false,
|
||||
shouldRunESRGAN: false,
|
||||
shouldRunGFPGAN: false,
|
||||
initialImagePath: '',
|
||||
maskPath: '',
|
||||
};
|
||||
|
||||
if (variation_amount > 0) {
|
||||
options.shouldGenerateVariations = true;
|
||||
options.variationAmount = variation_amount;
|
||||
if (with_variations) {
|
||||
options.seedWeights = seedWeightsToString(with_variations);
|
||||
}
|
||||
}
|
||||
|
||||
if (gfpgan_strength > 0) {
|
||||
options.shouldRunGFPGAN = true;
|
||||
options.gfpganStrength = gfpgan_strength;
|
||||
}
|
||||
|
||||
if (upscale) {
|
||||
options.shouldRunESRGAN = true;
|
||||
options.upscalingLevel = upscale[0];
|
||||
options.upscalingStrength = upscale[1];
|
||||
}
|
||||
|
||||
if (init_img) {
|
||||
options.shouldUseInitImage = true;
|
||||
options.initialImagePath = init_img;
|
||||
options.strength = strength;
|
||||
if (init_mask) {
|
||||
options.maskPath = init_mask;
|
||||
}
|
||||
}
|
||||
|
||||
// if we had a prompt, add all the metadata, but if we don't have a prompt,
|
||||
// we must have only done ESRGAN or GFPGAN so do not add that metadata
|
||||
if (prompt) {
|
||||
options.prompt = prompt;
|
||||
options.iterations = iterations;
|
||||
options.steps = steps;
|
||||
options.cfgScale = cfg_scale;
|
||||
options.height = height;
|
||||
options.width = width;
|
||||
options.sampler = sampler_name;
|
||||
options.seed = seed;
|
||||
options.seamless = seamless;
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
16
frontend/src/common/util/promptToString.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
|
||||
const promptToString = (prompt: InvokeAI.Prompt): string => {
|
||||
if (prompt.length === 1) {
|
||||
return prompt[0].prompt;
|
||||
}
|
||||
|
||||
return prompt
|
||||
.map(
|
||||
(promptItem: InvokeAI.PromptItem): string =>
|
||||
`${promptItem.prompt}:${promptItem.weight}`
|
||||
)
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
export default promptToString;
|
68
frontend/src/common/util/seedWeightPairs.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
|
||||
export const stringToSeedWeights = (
|
||||
string: string
|
||||
): InvokeAI.SeedWeights | boolean => {
|
||||
const stringPairs = string.split(',');
|
||||
const arrPairs = stringPairs.map((p) => p.split(':'));
|
||||
const pairs = arrPairs.map((p: Array<string>): InvokeAI.SeedWeightPair => {
|
||||
return { seed: parseInt(p[0]), weight: parseFloat(p[1]) };
|
||||
});
|
||||
|
||||
if (!validateSeedWeights(pairs)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return pairs;
|
||||
};
|
||||
|
||||
export const validateSeedWeights = (
|
||||
seedWeights: InvokeAI.SeedWeights | string
|
||||
): boolean => {
|
||||
return typeof seedWeights === 'string'
|
||||
? Boolean(stringToSeedWeights(seedWeights))
|
||||
: Boolean(
|
||||
seedWeights.length &&
|
||||
!seedWeights.some((pair: InvokeAI.SeedWeightPair) => {
|
||||
const { seed, weight } = pair;
|
||||
const isSeedValid = !isNaN(parseInt(seed.toString(), 10));
|
||||
const isWeightValid =
|
||||
!isNaN(parseInt(weight.toString(), 10)) &&
|
||||
weight >= 0 &&
|
||||
weight <= 1;
|
||||
return !(isSeedValid && isWeightValid);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const seedWeightsToString = (
|
||||
seedWeights: InvokeAI.SeedWeights
|
||||
): string => {
|
||||
return seedWeights.reduce((acc, pair, i, arr) => {
|
||||
const { seed, weight } = pair;
|
||||
acc += `${seed}:${weight}`;
|
||||
if (i !== arr.length - 1) {
|
||||
acc += ',';
|
||||
}
|
||||
return acc;
|
||||
}, '');
|
||||
};
|
||||
|
||||
export const seedWeightsToArray = (
|
||||
seedWeights: InvokeAI.SeedWeights
|
||||
): Array<Array<number>> => {
|
||||
return seedWeights.map((pair: InvokeAI.SeedWeightPair) => [
|
||||
pair.seed,
|
||||
pair.weight,
|
||||
]);
|
||||
};
|
||||
|
||||
export const stringToSeedWeightsArray = (
|
||||
string: string
|
||||
): Array<Array<number>> => {
|
||||
const stringPairs = string.split(',');
|
||||
const arrPairs = stringPairs.map((p) => p.split(':'));
|
||||
return arrPairs.map(
|
||||
(p: Array<string>): Array<number> => [parseInt(p[0]), parseFloat(p[1])]
|
||||
);
|
||||
};
|
@ -1,16 +0,0 @@
|
||||
import { Button, ButtonProps } from '@chakra-ui/react';
|
||||
|
||||
interface Props extends ButtonProps {
|
||||
label: string;
|
||||
}
|
||||
|
||||
const SDButton = (props: Props) => {
|
||||
const { label, size = 'sm', ...rest } = props;
|
||||
return (
|
||||
<Button size={size} {...rest}>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default SDButton;
|
@ -1,57 +0,0 @@
|
||||
import {
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Select,
|
||||
SelectProps,
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
interface Props extends SelectProps {
|
||||
label: string;
|
||||
validValues:
|
||||
| Array<number | string>
|
||||
| Array<{ key: string; value: string | number }>;
|
||||
}
|
||||
|
||||
const SDSelect = (props: Props) => {
|
||||
const {
|
||||
label,
|
||||
isDisabled,
|
||||
validValues,
|
||||
size = 'sm',
|
||||
fontSize = 'md',
|
||||
marginBottom = 1,
|
||||
whiteSpace = 'nowrap',
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<FormControl isDisabled={isDisabled}>
|
||||
<Flex justifyContent={'space-between'} alignItems={'center'}>
|
||||
<FormLabel
|
||||
marginBottom={marginBottom}
|
||||
>
|
||||
<Text fontSize={fontSize} whiteSpace={whiteSpace}>
|
||||
{label}
|
||||
</Text>
|
||||
</FormLabel>
|
||||
<Select fontSize={fontSize} size={size} {...rest}>
|
||||
{validValues.map((opt) => {
|
||||
return typeof opt === 'string' ||
|
||||
typeof opt === 'number' ? (
|
||||
<option key={opt} value={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
) : (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.key}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default SDSelect;
|
@ -1,161 +0,0 @@
|
||||
import { Center, Flex, Image, useColorModeValue } from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
import { RootState } from '../../app/store';
|
||||
import { setAllParameters, setInitialImagePath, setSeed } from '../sd/sdSlice';
|
||||
import { useState } from 'react';
|
||||
import ImageMetadataViewer from './ImageMetadataViewer';
|
||||
import DeleteImageModalButton from './DeleteImageModalButton';
|
||||
import SDButton from '../../components/SDButton';
|
||||
import { runESRGAN, runGFPGAN } from '../../app/socketio';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { SystemState } from '../system/systemSlice';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
const height = 'calc(100vh - 238px)';
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isProcessing: system.isProcessing,
|
||||
isConnected: system.isConnected,
|
||||
isGFPGANAvailable: system.isGFPGANAvailable,
|
||||
isESRGANAvailable: system.isESRGANAvailable,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const CurrentImage = () => {
|
||||
const { currentImage, intermediateImage } = useAppSelector(
|
||||
(state: RootState) => state.gallery
|
||||
);
|
||||
const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } =
|
||||
useAppSelector(systemSelector);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const bgColor = useColorModeValue(
|
||||
'rgba(255, 255, 255, 0.85)',
|
||||
'rgba(0, 0, 0, 0.8)'
|
||||
);
|
||||
|
||||
const [shouldShowImageDetails, setShouldShowImageDetails] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const imageToDisplay = intermediateImage || currentImage;
|
||||
|
||||
return (
|
||||
<Flex direction={'column'} rounded={'md'} borderWidth={1} p={2} gap={2}>
|
||||
{imageToDisplay && (
|
||||
<Flex gap={2}>
|
||||
<SDButton
|
||||
label='Use as initial image'
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
onClick={() =>
|
||||
dispatch(setInitialImagePath(imageToDisplay.url))
|
||||
}
|
||||
/>
|
||||
|
||||
<SDButton
|
||||
label='Use all'
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
onClick={() =>
|
||||
dispatch(setAllParameters(imageToDisplay.metadata))
|
||||
}
|
||||
/>
|
||||
|
||||
<SDButton
|
||||
label='Use seed'
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
isDisabled={!imageToDisplay.metadata.seed}
|
||||
onClick={() =>
|
||||
dispatch(setSeed(imageToDisplay.metadata.seed!))
|
||||
}
|
||||
/>
|
||||
|
||||
<SDButton
|
||||
label='Upscale'
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
isDisabled={
|
||||
!isESRGANAvailable ||
|
||||
Boolean(intermediateImage) ||
|
||||
!(isConnected && !isProcessing)
|
||||
}
|
||||
onClick={() => dispatch(runESRGAN(imageToDisplay))}
|
||||
/>
|
||||
<SDButton
|
||||
label='Fix faces'
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
isDisabled={
|
||||
!isGFPGANAvailable ||
|
||||
Boolean(intermediateImage) ||
|
||||
!(isConnected && !isProcessing)
|
||||
}
|
||||
onClick={() => dispatch(runGFPGAN(imageToDisplay))}
|
||||
/>
|
||||
<SDButton
|
||||
label='Details'
|
||||
colorScheme={'gray'}
|
||||
variant={shouldShowImageDetails ? 'solid' : 'outline'}
|
||||
borderWidth={1}
|
||||
flexGrow={1}
|
||||
onClick={() =>
|
||||
setShouldShowImageDetails(!shouldShowImageDetails)
|
||||
}
|
||||
/>
|
||||
<DeleteImageModalButton image={imageToDisplay}>
|
||||
<SDButton
|
||||
label='Delete'
|
||||
colorScheme={'red'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
isDisabled={Boolean(intermediateImage)}
|
||||
/>
|
||||
</DeleteImageModalButton>
|
||||
</Flex>
|
||||
)}
|
||||
<Center height={height} position={'relative'}>
|
||||
{imageToDisplay && (
|
||||
<Image
|
||||
src={imageToDisplay.url}
|
||||
fit='contain'
|
||||
maxWidth={'100%'}
|
||||
maxHeight={'100%'}
|
||||
/>
|
||||
)}
|
||||
{imageToDisplay && shouldShowImageDetails && (
|
||||
<Flex
|
||||
width={'100%'}
|
||||
height={'100%'}
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
p={3}
|
||||
boxSizing='border-box'
|
||||
backgroundColor={bgColor}
|
||||
overflow='scroll'
|
||||
>
|
||||
<ImageMetadataViewer image={imageToDisplay} />
|
||||
</Flex>
|
||||
)}
|
||||
</Center>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentImage;
|
155
frontend/src/features/gallery/CurrentImageButtons.tsx
Normal file
@ -0,0 +1,155 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import {
|
||||
setAllParameters,
|
||||
setInitialImagePath,
|
||||
setSeed,
|
||||
} from '../options/optionsSlice';
|
||||
import DeleteImageModal from './DeleteImageModal';
|
||||
import { SystemState } from '../system/systemSlice';
|
||||
import SDButton from '../../common/components/SDButton';
|
||||
import { runESRGAN, runGFPGAN } from '../../app/socketio/actions';
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isProcessing: system.isProcessing,
|
||||
isConnected: system.isConnected,
|
||||
isGFPGANAvailable: system.isGFPGANAvailable,
|
||||
isESRGANAvailable: system.isESRGANAvailable,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
type CurrentImageButtonsProps = {
|
||||
image: InvokeAI.Image;
|
||||
shouldShowImageDetails: boolean;
|
||||
setShouldShowImageDetails: (b: boolean) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Row of buttons for common actions:
|
||||
* Use as init image, use all params, use seed, upscale, fix faces, details, delete.
|
||||
*/
|
||||
const CurrentImageButtons = ({
|
||||
image,
|
||||
shouldShowImageDetails,
|
||||
setShouldShowImageDetails,
|
||||
}: CurrentImageButtonsProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { intermediateImage } = useAppSelector(
|
||||
(state: RootState) => state.gallery
|
||||
);
|
||||
|
||||
const { upscalingLevel, gfpganStrength } = useAppSelector(
|
||||
(state: RootState) => state.options
|
||||
);
|
||||
|
||||
const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } =
|
||||
useAppSelector(systemSelector);
|
||||
|
||||
const handleClickUseAsInitialImage = () =>
|
||||
dispatch(setInitialImagePath(image.url));
|
||||
|
||||
const handleClickUseAllParameters = () =>
|
||||
dispatch(setAllParameters(image.metadata));
|
||||
|
||||
// Non-null assertion: this button is disabled if there is no seed.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const handleClickUseSeed = () => dispatch(setSeed(image.metadata.image.seed));
|
||||
const handleClickUpscale = () => dispatch(runESRGAN(image));
|
||||
|
||||
const handleClickFixFaces = () => dispatch(runGFPGAN(image));
|
||||
|
||||
const handleClickShowImageDetails = () =>
|
||||
setShouldShowImageDetails(!shouldShowImageDetails);
|
||||
|
||||
return (
|
||||
<Flex gap={2}>
|
||||
<SDButton
|
||||
label="Use as initial image"
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
onClick={handleClickUseAsInitialImage}
|
||||
/>
|
||||
|
||||
<SDButton
|
||||
label="Use all"
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
isDisabled={!['txt2img', 'img2img'].includes(image.metadata.image.type)}
|
||||
onClick={handleClickUseAllParameters}
|
||||
/>
|
||||
|
||||
<SDButton
|
||||
label="Use seed"
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
isDisabled={!image.metadata.image.seed}
|
||||
onClick={handleClickUseSeed}
|
||||
/>
|
||||
|
||||
<SDButton
|
||||
label="Upscale"
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
isDisabled={
|
||||
!isESRGANAvailable ||
|
||||
Boolean(intermediateImage) ||
|
||||
!(isConnected && !isProcessing) ||
|
||||
!upscalingLevel
|
||||
}
|
||||
onClick={handleClickUpscale}
|
||||
/>
|
||||
<SDButton
|
||||
label="Fix faces"
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
isDisabled={
|
||||
!isGFPGANAvailable ||
|
||||
Boolean(intermediateImage) ||
|
||||
!(isConnected && !isProcessing) ||
|
||||
!gfpganStrength
|
||||
}
|
||||
onClick={handleClickFixFaces}
|
||||
/>
|
||||
<SDButton
|
||||
label="Details"
|
||||
colorScheme={'gray'}
|
||||
variant={shouldShowImageDetails ? 'solid' : 'outline'}
|
||||
borderWidth={1}
|
||||
flexGrow={1}
|
||||
onClick={handleClickShowImageDetails}
|
||||
/>
|
||||
<DeleteImageModal image={image}>
|
||||
<SDButton
|
||||
label="Delete"
|
||||
colorScheme={'red'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
isDisabled={Boolean(intermediateImage)}
|
||||
/>
|
||||
</DeleteImageModal>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentImageButtons;
|
67
frontend/src/features/gallery/CurrentImageDisplay.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import { Center, Flex, Image, Text, useColorModeValue } from '@chakra-ui/react';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import { useState } from 'react';
|
||||
import ImageMetadataViewer from './ImageMetadataViewer';
|
||||
import CurrentImageButtons from './CurrentImageButtons';
|
||||
|
||||
// TODO: With CSS Grid I had a hard time centering the image in a grid item. This is needed for that.
|
||||
const height = 'calc(100vh - 238px)';
|
||||
|
||||
/**
|
||||
* Displays the current image if there is one, plus associated actions.
|
||||
*/
|
||||
const CurrentImageDisplay = () => {
|
||||
const { currentImage, intermediateImage } = useAppSelector(
|
||||
(state: RootState) => state.gallery
|
||||
);
|
||||
|
||||
const bgColor = useColorModeValue(
|
||||
'rgba(255, 255, 255, 0.85)',
|
||||
'rgba(0, 0, 0, 0.8)'
|
||||
);
|
||||
|
||||
const [shouldShowImageDetails, setShouldShowImageDetails] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const imageToDisplay = intermediateImage || currentImage;
|
||||
|
||||
return imageToDisplay ? (
|
||||
<Flex direction={'column'} borderWidth={1} rounded={'md'} p={2} gap={2}>
|
||||
<CurrentImageButtons
|
||||
image={imageToDisplay}
|
||||
shouldShowImageDetails={shouldShowImageDetails}
|
||||
setShouldShowImageDetails={setShouldShowImageDetails}
|
||||
/>
|
||||
<Center height={height} position={'relative'}>
|
||||
<Image
|
||||
src={imageToDisplay.url}
|
||||
fit="contain"
|
||||
maxWidth={'100%'}
|
||||
maxHeight={'100%'}
|
||||
/>
|
||||
{shouldShowImageDetails && (
|
||||
<Flex
|
||||
width={'100%'}
|
||||
height={'100%'}
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
p={3}
|
||||
boxSizing="border-box"
|
||||
backgroundColor={bgColor}
|
||||
overflow="scroll"
|
||||
>
|
||||
<ImageMetadataViewer image={imageToDisplay} />
|
||||
</Flex>
|
||||
)}
|
||||
</Center>
|
||||
</Flex>
|
||||
) : (
|
||||
<Center height={'100%'} position={'relative'}>
|
||||
<Text size={'xl'}>No image selected</Text>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentImageDisplay;
|
125
frontend/src/features/gallery/DeleteImageModal.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import {
|
||||
Text,
|
||||
AlertDialog,
|
||||
AlertDialogBody,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogContent,
|
||||
AlertDialogOverlay,
|
||||
useDisclosure,
|
||||
Button,
|
||||
Switch,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Flex,
|
||||
} from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import {
|
||||
ChangeEvent,
|
||||
cloneElement,
|
||||
forwardRef,
|
||||
ReactElement,
|
||||
SyntheticEvent,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { deleteImage } from '../../app/socketio/actions';
|
||||
import { RootState } from '../../app/store';
|
||||
import { setShouldConfirmOnDelete, SystemState } from '../system/systemSlice';
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
|
||||
interface DeleteImageModalProps {
|
||||
/**
|
||||
* Component which, on click, should delete the image/open the modal.
|
||||
*/
|
||||
children: ReactElement;
|
||||
/**
|
||||
* The image to delete.
|
||||
*/
|
||||
image: InvokeAI.Image;
|
||||
}
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => system.shouldConfirmOnDelete
|
||||
);
|
||||
|
||||
/**
|
||||
* Needs a child, which will act as the button to delete an image.
|
||||
* If system.shouldConfirmOnDelete is true, a confirmation modal is displayed.
|
||||
* If it is false, the image is deleted immediately.
|
||||
* The confirmation modal has a "Don't ask me again" switch to set the boolean.
|
||||
*/
|
||||
const DeleteImageModal = forwardRef(
|
||||
({ image, children }: DeleteImageModalProps, ref) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const dispatch = useAppDispatch();
|
||||
const shouldConfirmOnDelete = useAppSelector(systemSelector);
|
||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleClickDelete = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
shouldConfirmOnDelete ? onOpen() : handleDelete();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
dispatch(deleteImage(image));
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleChangeShouldConfirmOnDelete = (
|
||||
e: ChangeEvent<HTMLInputElement>
|
||||
) => dispatch(setShouldConfirmOnDelete(!e.target.checked));
|
||||
|
||||
return (
|
||||
<>
|
||||
{cloneElement(children, {
|
||||
// TODO: This feels wrong.
|
||||
onClick: handleClickDelete,
|
||||
ref: ref,
|
||||
})}
|
||||
|
||||
<AlertDialog
|
||||
isOpen={isOpen}
|
||||
leastDestructiveRef={cancelRef}
|
||||
onClose={onClose}
|
||||
>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||
Delete image
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogBody>
|
||||
<Flex direction={'column'} gap={5}>
|
||||
<Text>
|
||||
Are you sure? You can't undo this action afterwards.
|
||||
</Text>
|
||||
<FormControl>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel mb={0}>Don't ask me again</FormLabel>
|
||||
<Switch
|
||||
checked={!shouldConfirmOnDelete}
|
||||
onChange={handleChangeShouldConfirmOnDelete}
|
||||
/>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
</Flex>
|
||||
</AlertDialogBody>
|
||||
<AlertDialogFooter>
|
||||
<Button ref={cancelRef} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button colorScheme="red" onClick={handleDelete} ml={3}>
|
||||
Delete
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default DeleteImageModal;
|
@ -1,94 +0,0 @@
|
||||
import {
|
||||
IconButtonProps,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Text,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import {
|
||||
cloneElement,
|
||||
ReactElement,
|
||||
SyntheticEvent,
|
||||
} from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
import { deleteImage } from '../../app/socketio';
|
||||
import { RootState } from '../../app/store';
|
||||
import SDButton from '../../components/SDButton';
|
||||
import { setShouldConfirmOnDelete, SystemState } from '../system/systemSlice';
|
||||
import { SDImage } from './gallerySlice';
|
||||
|
||||
interface Props extends IconButtonProps {
|
||||
image: SDImage;
|
||||
'aria-label': string;
|
||||
children: ReactElement;
|
||||
}
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => system.shouldConfirmOnDelete
|
||||
);
|
||||
|
||||
/*
|
||||
TODO: The modal and button to open it should be two different components,
|
||||
but their state is closely related and I'm not sure how best to accomplish it.
|
||||
*/
|
||||
const DeleteImageModalButton = (props: Omit<Props, 'aria-label'>) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const dispatch = useAppDispatch();
|
||||
const shouldConfirmOnDelete = useAppSelector(systemSelector);
|
||||
|
||||
const handleClickDelete = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
shouldConfirmOnDelete ? onOpen() : handleDelete();
|
||||
};
|
||||
|
||||
const { image, children } = props;
|
||||
|
||||
const handleDelete = () => {
|
||||
dispatch(deleteImage(image));
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDeleteAndDontAsk = () => {
|
||||
dispatch(deleteImage(image));
|
||||
dispatch(setShouldConfirmOnDelete(false));
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{cloneElement(children, {
|
||||
onClick: handleClickDelete,
|
||||
})}
|
||||
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Are you sure you want to delete this image?</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Text>It will be deleted forever!</Text>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter justifyContent={'space-between'}>
|
||||
<SDButton label={'Yes'} colorScheme='red' onClick={handleDelete} />
|
||||
<SDButton
|
||||
label={"Yes, and don't ask me again"}
|
||||
colorScheme='red'
|
||||
onClick={handleDeleteAndDontAsk}
|
||||
/>
|
||||
<SDButton label='Cancel' colorScheme='blue' onClick={onClose} />
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteImageModalButton;
|
143
frontend/src/features/gallery/HoverableImage.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Icon,
|
||||
IconButton,
|
||||
Image,
|
||||
Tooltip,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppDispatch } from '../../app/store';
|
||||
import { setCurrentImage } from './gallerySlice';
|
||||
import { FaCheck, FaSeedling, FaTrashAlt } from 'react-icons/fa';
|
||||
import DeleteImageModal from './DeleteImageModal';
|
||||
import { memo, SyntheticEvent, useState } from 'react';
|
||||
import { setAllParameters, setSeed } from '../options/optionsSlice';
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
|
||||
|
||||
interface HoverableImageProps {
|
||||
image: InvokeAI.Image;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
const memoEqualityCheck = (
|
||||
prev: HoverableImageProps,
|
||||
next: HoverableImageProps
|
||||
) => prev.image.uuid === next.image.uuid && prev.isSelected === next.isSelected;
|
||||
|
||||
/**
|
||||
* Gallery image component with delete/use all/use seed buttons on hover.
|
||||
*/
|
||||
const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
const [isHovered, setIsHovered] = useState<boolean>(false);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const checkColor = useColorModeValue('green.600', 'green.300');
|
||||
const bgColor = useColorModeValue('gray.200', 'gray.700');
|
||||
const bgGradient = useColorModeValue(
|
||||
'radial-gradient(circle, rgba(255,255,255,0.7) 0%, rgba(255,255,255,0.7) 20%, rgba(0,0,0,0) 100%)',
|
||||
'radial-gradient(circle, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.7) 20%, rgba(0,0,0,0) 100%)'
|
||||
);
|
||||
|
||||
const { image, isSelected } = props;
|
||||
const { url, uuid, metadata } = image;
|
||||
|
||||
const handleMouseOver = () => setIsHovered(true);
|
||||
const handleMouseOut = () => setIsHovered(false);
|
||||
|
||||
const handleClickSetAllParameters = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
dispatch(setAllParameters(metadata));
|
||||
};
|
||||
|
||||
const handleClickSetSeed = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
// Non-null assertion: this button is not rendered unless this exists
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
dispatch(setSeed(image.metadata.image.seed));
|
||||
};
|
||||
|
||||
const handleClickImage = () => dispatch(setCurrentImage(image));
|
||||
|
||||
return (
|
||||
<Box position={'relative'} key={uuid}>
|
||||
<Image
|
||||
width={120}
|
||||
height={120}
|
||||
objectFit="cover"
|
||||
rounded={'md'}
|
||||
src={url}
|
||||
loading={'lazy'}
|
||||
backgroundColor={bgColor}
|
||||
/>
|
||||
<Flex
|
||||
cursor={'pointer'}
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
rounded={'md'}
|
||||
width="100%"
|
||||
height="100%"
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
background={isSelected ? bgGradient : undefined}
|
||||
onClick={handleClickImage}
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseOut={handleMouseOut}
|
||||
>
|
||||
{isSelected && (
|
||||
<Icon fill={checkColor} width={'50%'} height={'50%'} as={FaCheck} />
|
||||
)}
|
||||
{isHovered && (
|
||||
<Flex
|
||||
direction={'column'}
|
||||
gap={1}
|
||||
position={'absolute'}
|
||||
top={1}
|
||||
right={1}
|
||||
>
|
||||
<Tooltip label={'Delete image'}>
|
||||
<DeleteImageModal image={image}>
|
||||
<IconButton
|
||||
colorScheme="red"
|
||||
aria-label="Delete image"
|
||||
icon={<FaTrashAlt />}
|
||||
size="xs"
|
||||
variant={'imageHoverIconButton'}
|
||||
fontSize={14}
|
||||
/>
|
||||
</DeleteImageModal>
|
||||
</Tooltip>
|
||||
{['txt2img', 'img2img'].includes(image.metadata.image.type) && (
|
||||
<Tooltip label="Use all parameters">
|
||||
<IconButton
|
||||
aria-label="Use all parameters"
|
||||
icon={<IoArrowUndoCircleOutline />}
|
||||
size="xs"
|
||||
fontSize={18}
|
||||
variant={'imageHoverIconButton'}
|
||||
onClickCapture={handleClickSetAllParameters}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{image.metadata.image.seed && (
|
||||
<Tooltip label="Use seed">
|
||||
<IconButton
|
||||
aria-label="Use seed"
|
||||
icon={<FaSeedling />}
|
||||
size="xs"
|
||||
fontSize={16}
|
||||
variant={'imageHoverIconButton'}
|
||||
onClickCapture={handleClickSetSeed}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}, memoEqualityCheck);
|
||||
|
||||
export default HoverableImage;
|
39
frontend/src/features/gallery/ImageGallery.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { Center, Flex, Text } from '@chakra-ui/react';
|
||||
import { RootState } from '../../app/store';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
import HoverableImage from './HoverableImage';
|
||||
|
||||
/**
|
||||
* Simple image gallery.
|
||||
*/
|
||||
const ImageGallery = () => {
|
||||
const { images, currentImageUuid } = useAppSelector(
|
||||
(state: RootState) => state.gallery
|
||||
);
|
||||
|
||||
/**
|
||||
* I don't like that this needs to rerender whenever the current image is changed.
|
||||
* What if we have a large number of images? I suppose pagination (planned) will
|
||||
* mitigate this issue.
|
||||
*
|
||||
* TODO: Refactor if performance complaints, or after migrating to new API which supports pagination.
|
||||
*/
|
||||
|
||||
return images.length ? (
|
||||
<Flex gap={2} wrap="wrap" pb={2}>
|
||||
{[...images].reverse().map((image) => {
|
||||
const { uuid } = image;
|
||||
const isSelected = currentImageUuid === uuid;
|
||||
return (
|
||||
<HoverableImage key={uuid} image={image} isSelected={isSelected} />
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
) : (
|
||||
<Center height={'100%'} position={'relative'}>
|
||||
<Text size={'xl'}>No images in gallery</Text>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageGallery;
|
@ -1,124 +1,326 @@
|
||||
import {
|
||||
Center,
|
||||
Flex,
|
||||
IconButton,
|
||||
Link,
|
||||
List,
|
||||
ListItem,
|
||||
Text,
|
||||
Box,
|
||||
Center,
|
||||
Flex,
|
||||
IconButton,
|
||||
Link,
|
||||
Text,
|
||||
Tooltip,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import { PARAMETERS } from '../../app/constants';
|
||||
import { useAppDispatch } from '../../app/hooks';
|
||||
import SDButton from '../../components/SDButton';
|
||||
import { setAllParameters, setParameter } from '../sd/sdSlice';
|
||||
import { SDImage, SDMetadata } from './gallerySlice';
|
||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||
import { memo } from 'react';
|
||||
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
|
||||
import { useAppDispatch } from '../../app/store';
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
import {
|
||||
setCfgScale,
|
||||
setGfpganStrength,
|
||||
setHeight,
|
||||
setImg2imgStrength,
|
||||
setInitialImagePath,
|
||||
setMaskPath,
|
||||
setPrompt,
|
||||
setSampler,
|
||||
setSeed,
|
||||
setSeedWeights,
|
||||
setShouldFitToWidthHeight,
|
||||
setSteps,
|
||||
setUpscalingLevel,
|
||||
setUpscalingStrength,
|
||||
setWidth,
|
||||
} from '../options/optionsSlice';
|
||||
import promptToString from '../../common/util/promptToString';
|
||||
import { seedWeightsToString } from '../../common/util/seedWeightPairs';
|
||||
import { FaCopy } from 'react-icons/fa';
|
||||
|
||||
type Props = {
|
||||
image: SDImage;
|
||||
type MetadataItemProps = {
|
||||
isLink?: boolean;
|
||||
label: string;
|
||||
onClick?: () => void;
|
||||
value: number | string | boolean;
|
||||
};
|
||||
|
||||
const ImageMetadataViewer = ({ image }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
/**
|
||||
* Component to display an individual metadata item or parameter.
|
||||
*/
|
||||
const MetadataItem = ({ label, value, onClick, isLink }: MetadataItemProps) => {
|
||||
return (
|
||||
<Flex gap={2}>
|
||||
{onClick && (
|
||||
<Tooltip label={`Recall ${label}`}>
|
||||
<IconButton
|
||||
aria-label="Use this parameter"
|
||||
icon={<IoArrowUndoCircleOutline />}
|
||||
size={'xs'}
|
||||
variant={'ghost'}
|
||||
fontSize={20}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Text fontWeight={'semibold'} whiteSpace={'nowrap'}>
|
||||
{label}:
|
||||
</Text>
|
||||
{isLink ? (
|
||||
<Link href={value.toString()} isExternal wordBreak={'break-all'}>
|
||||
{value.toString()} <ExternalLinkIcon mx="2px" />
|
||||
</Link>
|
||||
) : (
|
||||
<Text maxHeight={100} overflowY={'scroll'} wordBreak={'break-all'}>
|
||||
{value.toString()}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const keys = Object.keys(PARAMETERS);
|
||||
type ImageMetadataViewerProps = {
|
||||
image: InvokeAI.Image;
|
||||
};
|
||||
|
||||
const metadata: Array<{
|
||||
label: string;
|
||||
key: string;
|
||||
value: string | number | boolean;
|
||||
}> = [];
|
||||
// TODO: I don't know if this is needed.
|
||||
const memoEqualityCheck = (
|
||||
prev: ImageMetadataViewerProps,
|
||||
next: ImageMetadataViewerProps
|
||||
) => prev.image.uuid === next.image.uuid;
|
||||
|
||||
keys.forEach((key) => {
|
||||
const value = image.metadata[key as keyof SDMetadata];
|
||||
if (value !== undefined) {
|
||||
metadata.push({ label: PARAMETERS[key], key, value });
|
||||
}
|
||||
});
|
||||
// TODO: Show more interesting information in this component.
|
||||
|
||||
return (
|
||||
<Flex gap={2} direction={'column'} overflowY={'scroll'} width={'100%'}>
|
||||
<SDButton
|
||||
label='Use all parameters'
|
||||
colorScheme={'gray'}
|
||||
padding={2}
|
||||
isDisabled={metadata.length === 0}
|
||||
onClick={() => dispatch(setAllParameters(image.metadata))}
|
||||
/**
|
||||
* Image metadata viewer overlays currently selected image and provides
|
||||
* access to any of its metadata for use in processing.
|
||||
*/
|
||||
const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const jsonBgColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.100');
|
||||
|
||||
const metadata = image.metadata.image;
|
||||
const {
|
||||
type,
|
||||
postprocessing,
|
||||
sampler,
|
||||
prompt,
|
||||
seed,
|
||||
variations,
|
||||
steps,
|
||||
cfg_scale,
|
||||
seamless,
|
||||
width,
|
||||
height,
|
||||
strength,
|
||||
fit,
|
||||
init_image_path,
|
||||
mask_image_path,
|
||||
orig_path,
|
||||
scale,
|
||||
} = metadata;
|
||||
|
||||
const metadataJSON = JSON.stringify(metadata, null, 2);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
gap={1}
|
||||
direction={'column'}
|
||||
overflowY={'scroll'}
|
||||
width={'100%'}
|
||||
>
|
||||
<Flex gap={2}>
|
||||
<Text fontWeight={'semibold'}>File:</Text>
|
||||
<Link href={image.url} isExternal>
|
||||
{image.url}
|
||||
<ExternalLinkIcon mx="2px" />
|
||||
</Link>
|
||||
</Flex>
|
||||
{Object.keys(metadata).length ? (
|
||||
<>
|
||||
{type && <MetadataItem label="Type" value={type} />}
|
||||
{['esrgan', 'gfpgan'].includes(type) && (
|
||||
<MetadataItem label="Original image" value={orig_path} isLink />
|
||||
)}
|
||||
{type === 'gfpgan' && strength && (
|
||||
<MetadataItem
|
||||
label="Fix faces strength"
|
||||
value={strength}
|
||||
onClick={() => dispatch(setGfpganStrength(strength))}
|
||||
/>
|
||||
<Flex gap={2}>
|
||||
<Text fontWeight={'semibold'}>File:</Text>
|
||||
<Link href={image.url} isExternal>
|
||||
<Text>{image.url}</Text>
|
||||
</Link>
|
||||
</Flex>
|
||||
{metadata.length ? (
|
||||
<>
|
||||
<List>
|
||||
{metadata.map((parameter, i) => {
|
||||
const { label, key, value } = parameter;
|
||||
return (
|
||||
<ListItem key={i} pb={1}>
|
||||
<Flex gap={2}>
|
||||
<IconButton
|
||||
aria-label='Use this parameter'
|
||||
icon={<FaPlus />}
|
||||
size={'xs'}
|
||||
onClick={() =>
|
||||
dispatch(
|
||||
setParameter({
|
||||
key,
|
||||
value,
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Text fontWeight={'semibold'}>
|
||||
{label}:
|
||||
</Text>
|
||||
|
||||
{value === undefined ||
|
||||
value === null ||
|
||||
value === '' ||
|
||||
value === 0 ? (
|
||||
<Text
|
||||
maxHeight={100}
|
||||
fontStyle={'italic'}
|
||||
>
|
||||
None
|
||||
</Text>
|
||||
) : (
|
||||
<Text
|
||||
maxHeight={100}
|
||||
overflowY={'scroll'}
|
||||
>
|
||||
{value.toString()}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
<Flex gap={2}>
|
||||
<Text fontWeight={'semibold'}>Raw:</Text>
|
||||
<Text
|
||||
maxHeight={100}
|
||||
overflowY={'scroll'}
|
||||
wordBreak={'break-all'}
|
||||
>
|
||||
{JSON.stringify(image.metadata)}
|
||||
</Text>
|
||||
</Flex>
|
||||
</>
|
||||
) : (
|
||||
<Center width={'100%'} pt={10}>
|
||||
<Text fontSize={'lg'} fontWeight='semibold'>
|
||||
No metadata available
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
{type === 'esrgan' && scale && (
|
||||
<MetadataItem
|
||||
label="Upscaling scale"
|
||||
value={scale}
|
||||
onClick={() => dispatch(setUpscalingLevel(scale))}
|
||||
/>
|
||||
)}
|
||||
{type === 'esrgan' && strength && (
|
||||
<MetadataItem
|
||||
label="Upscaling strength"
|
||||
value={strength}
|
||||
onClick={() => dispatch(setUpscalingStrength(strength))}
|
||||
/>
|
||||
)}
|
||||
{prompt && (
|
||||
<MetadataItem
|
||||
label="Prompt"
|
||||
value={promptToString(prompt)}
|
||||
onClick={() => dispatch(setPrompt(prompt))}
|
||||
/>
|
||||
)}
|
||||
{seed && (
|
||||
<MetadataItem
|
||||
label="Seed"
|
||||
value={seed}
|
||||
onClick={() => dispatch(setSeed(seed))}
|
||||
/>
|
||||
)}
|
||||
{sampler && (
|
||||
<MetadataItem
|
||||
label="Sampler"
|
||||
value={sampler}
|
||||
onClick={() => dispatch(setSampler(sampler))}
|
||||
/>
|
||||
)}
|
||||
{steps && (
|
||||
<MetadataItem
|
||||
label="Steps"
|
||||
value={steps}
|
||||
onClick={() => dispatch(setSteps(steps))}
|
||||
/>
|
||||
)}
|
||||
{cfg_scale && (
|
||||
<MetadataItem
|
||||
label="CFG scale"
|
||||
value={cfg_scale}
|
||||
onClick={() => dispatch(setCfgScale(cfg_scale))}
|
||||
/>
|
||||
)}
|
||||
{variations && variations.length > 0 && (
|
||||
<MetadataItem
|
||||
label="Seed-weight pairs"
|
||||
value={seedWeightsToString(variations)}
|
||||
onClick={() =>
|
||||
dispatch(setSeedWeights(seedWeightsToString(variations)))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{seamless && (
|
||||
<MetadataItem
|
||||
label="Seamless"
|
||||
value={seamless}
|
||||
onClick={() => dispatch(setWidth(seamless))}
|
||||
/>
|
||||
)}
|
||||
{width && (
|
||||
<MetadataItem
|
||||
label="Width"
|
||||
value={width}
|
||||
onClick={() => dispatch(setWidth(width))}
|
||||
/>
|
||||
)}
|
||||
{height && (
|
||||
<MetadataItem
|
||||
label="Height"
|
||||
value={height}
|
||||
onClick={() => dispatch(setHeight(height))}
|
||||
/>
|
||||
)}
|
||||
{init_image_path && (
|
||||
<MetadataItem
|
||||
label="Initial image"
|
||||
value={init_image_path}
|
||||
isLink
|
||||
onClick={() => dispatch(setInitialImagePath(init_image_path))}
|
||||
/>
|
||||
)}
|
||||
{mask_image_path && (
|
||||
<MetadataItem
|
||||
label="Mask image"
|
||||
value={mask_image_path}
|
||||
isLink
|
||||
onClick={() => dispatch(setMaskPath(mask_image_path))}
|
||||
/>
|
||||
)}
|
||||
{type === 'img2img' && strength && (
|
||||
<MetadataItem
|
||||
label="Image to image strength"
|
||||
value={strength}
|
||||
onClick={() => dispatch(setImg2imgStrength(strength))}
|
||||
/>
|
||||
)}
|
||||
{fit && (
|
||||
<MetadataItem
|
||||
label="Image to image fit"
|
||||
value={fit}
|
||||
onClick={() => dispatch(setShouldFitToWidthHeight(fit))}
|
||||
/>
|
||||
)}
|
||||
{postprocessing &&
|
||||
postprocessing.length > 0 &&
|
||||
postprocessing.map(
|
||||
(postprocess: InvokeAI.PostProcessedImageMetadata) => {
|
||||
if (postprocess.type === 'esrgan') {
|
||||
const { scale, strength } = postprocess;
|
||||
return (
|
||||
<>
|
||||
<MetadataItem
|
||||
label="Upscaling scale"
|
||||
value={scale}
|
||||
onClick={() => dispatch(setUpscalingLevel(scale))}
|
||||
/>
|
||||
<MetadataItem
|
||||
label="Upscaling strength"
|
||||
value={strength}
|
||||
onClick={() => dispatch(setUpscalingStrength(strength))}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
} else if (postprocess.type === 'gfpgan') {
|
||||
const { strength } = postprocess;
|
||||
return (
|
||||
<MetadataItem
|
||||
label="Fix faces strength"
|
||||
value={strength}
|
||||
onClick={() => dispatch(setGfpganStrength(strength))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
<Flex gap={2} direction={'column'}>
|
||||
<Flex gap={2}>
|
||||
<Tooltip label={`Copy JSON`}>
|
||||
<IconButton
|
||||
aria-label="Copy JSON"
|
||||
icon={<FaCopy />}
|
||||
size={'xs'}
|
||||
variant={'ghost'}
|
||||
fontSize={14}
|
||||
onClick={() => navigator.clipboard.writeText(metadataJSON)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Text fontWeight={'semibold'}>JSON:</Text>
|
||||
</Flex>
|
||||
<Box
|
||||
// maxHeight={200}
|
||||
overflow={'scroll'}
|
||||
flexGrow={3}
|
||||
wordBreak={'break-all'}
|
||||
bgColor={jsonBgColor}
|
||||
padding={2}
|
||||
>
|
||||
<pre>{metadataJSON}</pre>
|
||||
</Box>
|
||||
</Flex>
|
||||
</>
|
||||
) : (
|
||||
<Center width={'100%'} pt={10}>
|
||||
<Text fontSize={'lg'} fontWeight="semibold">
|
||||
No metadata available
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}, memoEqualityCheck);
|
||||
|
||||
export default ImageMetadataViewer;
|
||||
|
@ -1,150 +0,0 @@
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Icon,
|
||||
IconButton,
|
||||
Image,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { RootState } from '../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
import { SDImage, setCurrentImage } from './gallerySlice';
|
||||
import { FaCheck, FaCopy, FaSeedling, FaTrash } from 'react-icons/fa';
|
||||
import DeleteImageModalButton from './DeleteImageModalButton';
|
||||
import { memo, SyntheticEvent, useState } from 'react';
|
||||
import { setAllParameters, setSeed } from '../sd/sdSlice';
|
||||
|
||||
interface HoverableImageProps {
|
||||
image: SDImage;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
const HoverableImage = memo(
|
||||
(props: HoverableImageProps) => {
|
||||
const [isHovered, setIsHovered] = useState<boolean>(false);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const checkColor = useColorModeValue('green.600', 'green.300');
|
||||
const bgColor = useColorModeValue('gray.200', 'gray.700');
|
||||
const bgGradient = useColorModeValue(
|
||||
'radial-gradient(circle, rgba(255,255,255,0.7) 0%, rgba(255,255,255,0.7) 20%, rgba(0,0,0,0) 100%)',
|
||||
'radial-gradient(circle, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.7) 20%, rgba(0,0,0,0) 100%)'
|
||||
);
|
||||
|
||||
const { image, isSelected } = props;
|
||||
const { url, uuid, metadata } = image;
|
||||
|
||||
const handleMouseOver = () => setIsHovered(true);
|
||||
const handleMouseOut = () => setIsHovered(false);
|
||||
const handleClickSetAllParameters = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
dispatch(setAllParameters(metadata));
|
||||
};
|
||||
const handleClickSetSeed = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
dispatch(setSeed(image.metadata.seed!)); // component not rendered unless this exists
|
||||
};
|
||||
|
||||
return (
|
||||
<Box position={'relative'} key={uuid}>
|
||||
<Image
|
||||
width={120}
|
||||
height={120}
|
||||
objectFit='cover'
|
||||
rounded={'md'}
|
||||
src={url}
|
||||
loading={'lazy'}
|
||||
backgroundColor={bgColor}
|
||||
/>
|
||||
<Flex
|
||||
cursor={'pointer'}
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
rounded={'md'}
|
||||
width='100%'
|
||||
height='100%'
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
background={isSelected ? bgGradient : undefined}
|
||||
onClick={() => dispatch(setCurrentImage(image))}
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseOut={handleMouseOut}
|
||||
>
|
||||
{isSelected && (
|
||||
<Icon
|
||||
fill={checkColor}
|
||||
width={'50%'}
|
||||
height={'50%'}
|
||||
as={FaCheck}
|
||||
/>
|
||||
)}
|
||||
{isHovered && (
|
||||
<Flex
|
||||
direction={'column'}
|
||||
gap={1}
|
||||
position={'absolute'}
|
||||
top={1}
|
||||
right={1}
|
||||
>
|
||||
<DeleteImageModalButton image={image}>
|
||||
<IconButton
|
||||
colorScheme='red'
|
||||
aria-label='Delete image'
|
||||
icon={<FaTrash />}
|
||||
size='xs'
|
||||
fontSize={15}
|
||||
/>
|
||||
</DeleteImageModalButton>
|
||||
<IconButton
|
||||
aria-label='Use all parameters'
|
||||
colorScheme={'blue'}
|
||||
icon={<FaCopy />}
|
||||
size='xs'
|
||||
fontSize={15}
|
||||
onClickCapture={handleClickSetAllParameters}
|
||||
/>
|
||||
{image.metadata.seed && (
|
||||
<IconButton
|
||||
aria-label='Use seed'
|
||||
colorScheme={'blue'}
|
||||
icon={<FaSeedling />}
|
||||
size='xs'
|
||||
fontSize={16}
|
||||
onClickCapture={handleClickSetSeed}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
(prev, next) =>
|
||||
prev.image.uuid === next.image.uuid &&
|
||||
prev.isSelected === next.isSelected
|
||||
);
|
||||
|
||||
const ImageRoll = () => {
|
||||
const { images, currentImageUuid } = useAppSelector(
|
||||
(state: RootState) => state.gallery
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex gap={2} wrap='wrap' pb={2}>
|
||||
{[...images].reverse().map((image) => {
|
||||
const { uuid } = image;
|
||||
const isSelected = currentImageUuid === uuid;
|
||||
return (
|
||||
<HoverableImage
|
||||
key={uuid}
|
||||
image={image}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageRoll;
|
@ -1,40 +1,13 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { UpscalingLevel } from '../sd/sdSlice';
|
||||
import { backendToFrontendParameters } from '../../app/parameterTranslation';
|
||||
|
||||
// TODO: Revise pending metadata RFC: https://github.com/lstein/stable-diffusion/issues/266
|
||||
export interface SDMetadata {
|
||||
prompt?: string;
|
||||
steps?: number;
|
||||
cfgScale?: number;
|
||||
height?: number;
|
||||
width?: number;
|
||||
sampler?: string;
|
||||
seed?: number;
|
||||
img2imgStrength?: number;
|
||||
gfpganStrength?: number;
|
||||
upscalingLevel?: UpscalingLevel;
|
||||
upscalingStrength?: number;
|
||||
initialImagePath?: string;
|
||||
maskPath?: string;
|
||||
seamless?: boolean;
|
||||
shouldFitToWidthHeight?: boolean;
|
||||
}
|
||||
|
||||
export interface SDImage {
|
||||
// TODO: I have installed @types/uuid but cannot figure out how to use them here.
|
||||
uuid: string;
|
||||
url: string;
|
||||
metadata: SDMetadata;
|
||||
}
|
||||
import { clamp } from 'lodash';
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
|
||||
export interface GalleryState {
|
||||
currentImage?: InvokeAI.Image;
|
||||
currentImageUuid: string;
|
||||
images: Array<SDImage>;
|
||||
intermediateImage?: SDImage;
|
||||
currentImage?: SDImage;
|
||||
images: Array<InvokeAI.Image>;
|
||||
intermediateImage?: InvokeAI.Image;
|
||||
}
|
||||
|
||||
const initialState: GalleryState = {
|
||||
@ -46,99 +19,84 @@ export const gallerySlice = createSlice({
|
||||
name: 'gallery',
|
||||
initialState,
|
||||
reducers: {
|
||||
setCurrentImage: (state, action: PayloadAction<SDImage>) => {
|
||||
setCurrentImage: (state, action: PayloadAction<InvokeAI.Image>) => {
|
||||
state.currentImage = action.payload;
|
||||
state.currentImageUuid = action.payload.uuid;
|
||||
},
|
||||
removeImage: (state, action: PayloadAction<SDImage>) => {
|
||||
const { uuid } = action.payload;
|
||||
removeImage: (state, action: PayloadAction<string>) => {
|
||||
const uuid = action.payload;
|
||||
|
||||
const newImages = state.images.filter((image) => image.uuid !== uuid);
|
||||
|
||||
const imageToDeleteIndex = state.images.findIndex(
|
||||
(image) => image.uuid === uuid
|
||||
);
|
||||
if (uuid === state.currentImageUuid) {
|
||||
/**
|
||||
* We are deleting the currently selected image.
|
||||
*
|
||||
* We want the new currentl selected image to be under the cursor in the
|
||||
* gallery, so we need to do some fanagling. The currently selected image
|
||||
* is set by its UUID, not its index in the image list.
|
||||
*
|
||||
* Get the currently selected image's index.
|
||||
*/
|
||||
const imageToDeleteIndex = state.images.findIndex(
|
||||
(image) => image.uuid === uuid
|
||||
);
|
||||
|
||||
const newCurrentImageIndex = Math.min(
|
||||
Math.max(imageToDeleteIndex, 0),
|
||||
newImages.length - 1
|
||||
);
|
||||
/**
|
||||
* New current image needs to be in the same spot, but because the gallery
|
||||
* is sorted in reverse order, the new current image's index will actuall be
|
||||
* one less than the deleted image's index.
|
||||
*
|
||||
* Clamp the new index to ensure it is valid..
|
||||
*/
|
||||
const newCurrentImageIndex = clamp(
|
||||
imageToDeleteIndex - 1,
|
||||
0,
|
||||
newImages.length - 1
|
||||
);
|
||||
|
||||
state.currentImage = newImages.length
|
||||
? newImages[newCurrentImageIndex]
|
||||
: undefined;
|
||||
|
||||
state.currentImageUuid = newImages.length
|
||||
? newImages[newCurrentImageIndex].uuid
|
||||
: '';
|
||||
}
|
||||
|
||||
state.images = newImages;
|
||||
|
||||
state.currentImage = newImages.length
|
||||
? newImages[newCurrentImageIndex]
|
||||
: undefined;
|
||||
|
||||
state.currentImageUuid = newImages.length
|
||||
? newImages[newCurrentImageIndex].uuid
|
||||
: '';
|
||||
},
|
||||
addImage: (state, action: PayloadAction<SDImage>) => {
|
||||
addImage: (state, action: PayloadAction<InvokeAI.Image>) => {
|
||||
state.images.push(action.payload);
|
||||
state.currentImageUuid = action.payload.uuid;
|
||||
state.intermediateImage = undefined;
|
||||
state.currentImage = action.payload;
|
||||
},
|
||||
setIntermediateImage: (state, action: PayloadAction<SDImage>) => {
|
||||
setIntermediateImage: (state, action: PayloadAction<InvokeAI.Image>) => {
|
||||
state.intermediateImage = action.payload;
|
||||
},
|
||||
clearIntermediateImage: (state) => {
|
||||
state.intermediateImage = undefined;
|
||||
},
|
||||
setGalleryImages: (
|
||||
state,
|
||||
action: PayloadAction<
|
||||
Array<{
|
||||
path: string;
|
||||
metadata: { [key: string]: string | number | boolean };
|
||||
}>
|
||||
>
|
||||
) => {
|
||||
// TODO: Revise pending metadata RFC: https://github.com/lstein/stable-diffusion/issues/266
|
||||
const images = action.payload;
|
||||
|
||||
if (images.length === 0) {
|
||||
// there are no images on disk, clear the gallery
|
||||
state.images = [];
|
||||
state.currentImageUuid = '';
|
||||
state.currentImage = undefined;
|
||||
} else {
|
||||
// Filter image urls that are already in the rehydrated state
|
||||
const filteredImages = action.payload.filter(
|
||||
(image) => !state.images.find((i) => i.url === image.path)
|
||||
);
|
||||
|
||||
const preparedImages = filteredImages.map((image): SDImage => {
|
||||
return {
|
||||
uuid: uuidv4(),
|
||||
url: image.path,
|
||||
metadata: backendToFrontendParameters(image.metadata),
|
||||
};
|
||||
});
|
||||
|
||||
const newImages = [...state.images].concat(preparedImages);
|
||||
|
||||
// if previous currentimage no longer exists, set a new one
|
||||
if (!newImages.find((image) => image.uuid === state.currentImageUuid)) {
|
||||
const newCurrentImage = newImages[newImages.length - 1];
|
||||
state.currentImage = newCurrentImage;
|
||||
state.currentImageUuid = newCurrentImage.uuid;
|
||||
}
|
||||
|
||||
setGalleryImages: (state, action: PayloadAction<Array<InvokeAI.Image>>) => {
|
||||
const newImages = action.payload;
|
||||
if (newImages.length) {
|
||||
const newCurrentImage = newImages[newImages.length - 1];
|
||||
state.images = newImages;
|
||||
state.currentImage = newCurrentImage;
|
||||
state.currentImageUuid = newCurrentImage.uuid;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setCurrentImage,
|
||||
removeImage,
|
||||
addImage,
|
||||
clearIntermediateImage,
|
||||
removeImage,
|
||||
setCurrentImage,
|
||||
setGalleryImages,
|
||||
setIntermediateImage,
|
||||
clearIntermediateImage,
|
||||
} = gallerySlice.actions;
|
||||
|
||||
export default gallerySlice.reducer;
|
||||
|
@ -1,35 +0,0 @@
|
||||
import { Progress } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { useAppSelector } from '../../app/hooks';
|
||||
import { RootState } from '../../app/store';
|
||||
import { SDState } from '../sd/sdSlice';
|
||||
|
||||
const sdSelector = createSelector(
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
realSteps: sd.realSteps,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const ProgressBar = () => {
|
||||
const { realSteps } = useAppSelector(sdSelector);
|
||||
const { currentStep } = useAppSelector((state: RootState) => state.system);
|
||||
const progress = Math.round((currentStep * 100) / realSteps);
|
||||
return (
|
||||
<Progress
|
||||
height='10px'
|
||||
value={progress}
|
||||
isIndeterminate={progress < 0 || currentStep === realSteps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProgressBar;
|
@ -1,93 +0,0 @@
|
||||
import {
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
Link,
|
||||
Spacer,
|
||||
Text,
|
||||
useColorMode,
|
||||
} from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { FaSun, FaMoon, FaGithub } from 'react-icons/fa';
|
||||
import { MdHelp, MdSettings } from 'react-icons/md';
|
||||
import { useAppSelector } from '../../app/hooks';
|
||||
import { RootState } from '../../app/store';
|
||||
import SettingsModal from '../system/SettingsModal';
|
||||
import { SystemState } from '../system/systemSlice';
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return { isConnected: system.isConnected };
|
||||
},
|
||||
{
|
||||
memoizeOptions: { resultEqualityCheck: isEqual },
|
||||
}
|
||||
);
|
||||
|
||||
const SiteHeader = () => {
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
const { isConnected } = useAppSelector(systemSelector);
|
||||
|
||||
return (
|
||||
<Flex minWidth='max-content' alignItems='center' gap='1' pl={2} pr={1}>
|
||||
<Heading size={'lg'}>Stable Diffusion Dream Server</Heading>
|
||||
|
||||
<Spacer />
|
||||
|
||||
<Text textColor={isConnected ? 'green.500' : 'red.500'}>
|
||||
{isConnected ? `Connected to server` : 'No connection to server'}
|
||||
</Text>
|
||||
|
||||
<SettingsModal>
|
||||
<IconButton
|
||||
aria-label='Settings'
|
||||
variant='link'
|
||||
fontSize={24}
|
||||
size={'sm'}
|
||||
icon={<MdSettings />}
|
||||
/>
|
||||
</SettingsModal>
|
||||
|
||||
<IconButton
|
||||
aria-label='Link to Github Issues'
|
||||
variant='link'
|
||||
fontSize={23}
|
||||
size={'sm'}
|
||||
icon={
|
||||
<Link
|
||||
isExternal
|
||||
href='http://github.com/lstein/stable-diffusion/issues'
|
||||
>
|
||||
<MdHelp />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
aria-label='Link to Github Repo'
|
||||
variant='link'
|
||||
fontSize={20}
|
||||
size={'sm'}
|
||||
icon={
|
||||
<Link isExternal href='http://github.com/lstein/stable-diffusion'>
|
||||
<FaGithub />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
aria-label='Toggle Dark Mode'
|
||||
onClick={toggleColorMode}
|
||||
variant='link'
|
||||
size={'sm'}
|
||||
fontSize={colorMode == 'light' ? 18 : 20}
|
||||
icon={colorMode == 'light' ? <FaMoon /> : <FaSun />}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default SiteHeader;
|
87
frontend/src/features/options/ESRGANOptions.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
|
||||
import { RootState } from '../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
|
||||
import {
|
||||
setUpscalingLevel,
|
||||
setUpscalingStrength,
|
||||
UpscalingLevel,
|
||||
OptionsState,
|
||||
} from '../options/optionsSlice';
|
||||
|
||||
|
||||
import { UPSCALING_LEVELS } from '../../app/constants';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { SystemState } from '../system/systemSlice';
|
||||
import { ChangeEvent } from 'react';
|
||||
import SDNumberInput from '../../common/components/SDNumberInput';
|
||||
import SDSelect from '../../common/components/SDSelect';
|
||||
|
||||
const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
return {
|
||||
upscalingLevel: options.upscalingLevel,
|
||||
upscalingStrength: options.upscalingStrength,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isESRGANAvailable: system.isESRGANAvailable,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Displays upscaling/ESRGAN options (level and strength).
|
||||
*/
|
||||
const ESRGANOptions = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { upscalingLevel, upscalingStrength } = useAppSelector(optionsSelector);
|
||||
const { isESRGANAvailable } = useAppSelector(systemSelector);
|
||||
|
||||
const handleChangeLevel = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||
dispatch(setUpscalingLevel(Number(e.target.value) as UpscalingLevel));
|
||||
|
||||
const handleChangeStrength = (v: string | number) =>
|
||||
dispatch(setUpscalingStrength(Number(v)));
|
||||
|
||||
return (
|
||||
<Flex direction={'column'} gap={2}>
|
||||
<SDSelect
|
||||
isDisabled={!isESRGANAvailable}
|
||||
label="Scale"
|
||||
value={upscalingLevel}
|
||||
onChange={handleChangeLevel}
|
||||
validValues={UPSCALING_LEVELS}
|
||||
/>
|
||||
<SDNumberInput
|
||||
isDisabled={!isESRGANAvailable}
|
||||
label="Strength"
|
||||
step={0.05}
|
||||
min={0}
|
||||
max={1}
|
||||
onChange={handleChangeStrength}
|
||||
value={upscalingStrength}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ESRGANOptions;
|
68
frontend/src/features/options/GFPGANOptions.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
|
||||
import { RootState } from '../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
|
||||
import { OptionsState, setGfpganStrength } from '../options/optionsSlice';
|
||||
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { SystemState } from '../system/systemSlice';
|
||||
import SDNumberInput from '../../common/components/SDNumberInput';
|
||||
|
||||
const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
return {
|
||||
gfpganStrength: options.gfpganStrength,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isGFPGANAvailable: system.isGFPGANAvailable,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Displays face-fixing/GFPGAN options (strength).
|
||||
*/
|
||||
const GFPGANOptions = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { gfpganStrength } = useAppSelector(optionsSelector);
|
||||
const { isGFPGANAvailable } = useAppSelector(systemSelector);
|
||||
|
||||
const handleChangeStrength = (v: string | number) =>
|
||||
dispatch(setGfpganStrength(Number(v)));
|
||||
|
||||
return (
|
||||
<Flex direction={'column'} gap={2}>
|
||||
<SDNumberInput
|
||||
isDisabled={!isGFPGANAvailable}
|
||||
label="Strength"
|
||||
step={0.05}
|
||||
min={0}
|
||||
max={1}
|
||||
onChange={handleChangeStrength}
|
||||
value={gfpganStrength}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default GFPGANOptions;
|
59
frontend/src/features/options/ImageToImageOptions.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import SDNumberInput from '../../common/components/SDNumberInput';
|
||||
import SDSwitch from '../../common/components/SDSwitch';
|
||||
import InitAndMaskImage from './InitAndMaskImage';
|
||||
import {
|
||||
OptionsState,
|
||||
setImg2imgStrength,
|
||||
setShouldFitToWidthHeight,
|
||||
} from './optionsSlice';
|
||||
|
||||
const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
return {
|
||||
img2imgStrength: options.img2imgStrength,
|
||||
shouldFitToWidthHeight: options.shouldFitToWidthHeight,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Options for img2img generation (strength, fit, init/mask upload).
|
||||
*/
|
||||
const ImageToImageOptions = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { img2imgStrength, shouldFitToWidthHeight } =
|
||||
useAppSelector(optionsSelector);
|
||||
|
||||
const handleChangeStrength = (v: string | number) =>
|
||||
dispatch(setImg2imgStrength(Number(v)));
|
||||
|
||||
const handleChangeFit = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldFitToWidthHeight(e.target.checked));
|
||||
|
||||
return (
|
||||
<Flex direction={'column'} gap={2}>
|
||||
<SDNumberInput
|
||||
label="Strength"
|
||||
step={0.01}
|
||||
min={0}
|
||||
max={1}
|
||||
onChange={handleChangeStrength}
|
||||
value={img2imgStrength}
|
||||
/>
|
||||
<SDSwitch
|
||||
label="Fit initial image to output size"
|
||||
isChecked={shouldFitToWidthHeight}
|
||||
onChange={handleChangeFit}
|
||||
/>
|
||||
<InitAndMaskImage />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageToImageOptions;
|
64
frontend/src/features/options/ImageUploader.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { cloneElement, ReactElement, SyntheticEvent, useCallback } from 'react';
|
||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
||||
|
||||
type ImageUploaderProps = {
|
||||
/**
|
||||
* Component which, on click, should open the upload interface.
|
||||
*/
|
||||
children: ReactElement;
|
||||
/**
|
||||
* Callback to handle uploading the selected file.
|
||||
*/
|
||||
fileAcceptedCallback: (file: File) => void;
|
||||
/**
|
||||
* Callback to handle a file being rejected.
|
||||
*/
|
||||
fileRejectionCallback: (rejection: FileRejection) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* File upload using react-dropzone.
|
||||
* Needs a child to be the button to activate the upload interface.
|
||||
*/
|
||||
const ImageUploader = ({
|
||||
children,
|
||||
fileAcceptedCallback,
|
||||
fileRejectionCallback,
|
||||
}: ImageUploaderProps) => {
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: Array<File>, fileRejections: Array<FileRejection>) => {
|
||||
fileRejections.forEach((rejection: FileRejection) => {
|
||||
fileRejectionCallback(rejection);
|
||||
});
|
||||
|
||||
acceptedFiles.forEach((file: File) => {
|
||||
fileAcceptedCallback(file);
|
||||
});
|
||||
},
|
||||
[fileAcceptedCallback, fileRejectionCallback]
|
||||
);
|
||||
|
||||
const { getRootProps, getInputProps, open } = useDropzone({
|
||||
onDrop,
|
||||
accept: {
|
||||
'image/jpeg': ['.jpg', '.jpeg', '.png'],
|
||||
},
|
||||
});
|
||||
|
||||
const handleClickUploadIcon = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
open();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box {...getRootProps()} flexGrow={3}>
|
||||
<input {...getInputProps({ multiple: false })} />
|
||||
{cloneElement(children, {
|
||||
onClick: handleClickUploadIcon,
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageUploader;
|
57
frontend/src/features/options/InitAndMaskImage.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { Flex, Image } from '@chakra-ui/react';
|
||||
import { useState } from 'react';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import { OptionsState } from '../../features/options/optionsSlice';
|
||||
import './InitAndMaskImage.css';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import InitAndMaskUploadButtons from './InitAndMaskUploadButtons';
|
||||
|
||||
const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
return {
|
||||
initialImagePath: options.initialImagePath,
|
||||
maskPath: options.maskPath,
|
||||
};
|
||||
},
|
||||
{ memoizeOptions: { resultEqualityCheck: isEqual } }
|
||||
);
|
||||
|
||||
/**
|
||||
* Displays init and mask images and buttons to upload/delete them.
|
||||
*/
|
||||
const InitAndMaskImage = () => {
|
||||
const { initialImagePath, maskPath } = useAppSelector(optionsSelector);
|
||||
const [shouldShowMask, setShouldShowMask] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<Flex direction={'column'} alignItems={'center'} gap={2}>
|
||||
<InitAndMaskUploadButtons setShouldShowMask={setShouldShowMask} />
|
||||
{initialImagePath && (
|
||||
<Flex position={'relative'} width={'100%'}>
|
||||
<Image
|
||||
fit={'contain'}
|
||||
src={initialImagePath}
|
||||
rounded={'md'}
|
||||
className={'checkerboard'}
|
||||
/>
|
||||
{shouldShowMask && maskPath && (
|
||||
<Image
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
fit={'contain'}
|
||||
src={maskPath}
|
||||
rounded={'md'}
|
||||
zIndex={1}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default InitAndMaskImage;
|
151
frontend/src/features/options/InitAndMaskUploadButtons.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import { Button, Flex, IconButton, useToast } from '@chakra-ui/react';
|
||||
import { SyntheticEvent, useCallback } from 'react';
|
||||
import { FaTrash, FaUpload } from 'react-icons/fa';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import {
|
||||
OptionsState,
|
||||
setInitialImagePath,
|
||||
setMaskPath,
|
||||
} from '../../features/options/optionsSlice';
|
||||
import {
|
||||
uploadInitialImage,
|
||||
uploadMaskImage,
|
||||
} from '../../app/socketio/actions';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import ImageUploader from './ImageUploader';
|
||||
import { FileRejection } from 'react-dropzone';
|
||||
|
||||
const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
return {
|
||||
initialImagePath: options.initialImagePath,
|
||||
maskPath: options.maskPath,
|
||||
};
|
||||
},
|
||||
{ memoizeOptions: { resultEqualityCheck: isEqual } }
|
||||
);
|
||||
|
||||
type InitAndMaskUploadButtonsProps = {
|
||||
setShouldShowMask: (b: boolean) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Init and mask image upload buttons.
|
||||
*/
|
||||
const InitAndMaskUploadButtons = ({
|
||||
setShouldShowMask,
|
||||
}: InitAndMaskUploadButtonsProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { initialImagePath, maskPath } = useAppSelector(optionsSelector);
|
||||
|
||||
// Use a toast to alert user when a file upload is rejected
|
||||
const toast = useToast();
|
||||
|
||||
// Clear the init and mask images
|
||||
const handleClickResetInitialImage = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
dispatch(setInitialImagePath(''));
|
||||
};
|
||||
|
||||
// Clear the init and mask images
|
||||
const handleClickResetMask = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
dispatch(setMaskPath(''));
|
||||
};
|
||||
|
||||
// Handle hover to view initial image and mask image
|
||||
const handleMouseOverInitialImageUploadButton = () =>
|
||||
setShouldShowMask(false);
|
||||
const handleMouseOutInitialImageUploadButton = () => setShouldShowMask(true);
|
||||
|
||||
const handleMouseOverMaskUploadButton = () => setShouldShowMask(true);
|
||||
const handleMouseOutMaskUploadButton = () => setShouldShowMask(true);
|
||||
|
||||
// Callbacks to for handling file upload attempts
|
||||
const initImageFileAcceptedCallback = useCallback(
|
||||
(file: File) => dispatch(uploadInitialImage(file)),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const maskImageFileAcceptedCallback = useCallback(
|
||||
(file: File) => dispatch(uploadMaskImage(file)),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const fileRejectionCallback = useCallback(
|
||||
(rejection: FileRejection) => {
|
||||
const msg = rejection.errors.reduce(
|
||||
(acc: string, cur: { message: string }) => acc + '\n' + cur.message,
|
||||
''
|
||||
);
|
||||
|
||||
toast({
|
||||
title: 'Upload failed',
|
||||
description: msg,
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
});
|
||||
},
|
||||
[toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex gap={2} justifyContent={'space-between'} width={'100%'}>
|
||||
<ImageUploader
|
||||
fileAcceptedCallback={initImageFileAcceptedCallback}
|
||||
fileRejectionCallback={fileRejectionCallback}
|
||||
>
|
||||
<Button
|
||||
size={'sm'}
|
||||
fontSize={'md'}
|
||||
fontWeight={'normal'}
|
||||
onMouseOver={handleMouseOverInitialImageUploadButton}
|
||||
onMouseOut={handleMouseOutInitialImageUploadButton}
|
||||
leftIcon={<FaUpload />}
|
||||
width={'100%'}
|
||||
>
|
||||
Image
|
||||
</Button>
|
||||
</ImageUploader>
|
||||
|
||||
<IconButton
|
||||
isDisabled={!initialImagePath}
|
||||
size={'sm'}
|
||||
aria-label={'Reset mask'}
|
||||
onClick={handleClickResetInitialImage}
|
||||
icon={<FaTrash />}
|
||||
/>
|
||||
|
||||
<ImageUploader
|
||||
fileAcceptedCallback={maskImageFileAcceptedCallback}
|
||||
fileRejectionCallback={fileRejectionCallback}
|
||||
>
|
||||
<Button
|
||||
isDisabled={!initialImagePath}
|
||||
size={'sm'}
|
||||
fontSize={'md'}
|
||||
fontWeight={'normal'}
|
||||
onMouseOver={handleMouseOverMaskUploadButton}
|
||||
onMouseOut={handleMouseOutMaskUploadButton}
|
||||
leftIcon={<FaUpload />}
|
||||
width={'100%'}
|
||||
>
|
||||
Mask
|
||||
</Button>
|
||||
</ImageUploader>
|
||||
|
||||
<IconButton
|
||||
isDisabled={!maskPath}
|
||||
size={'sm'}
|
||||
aria-label={'Reset mask'}
|
||||
onClick={handleClickResetMask}
|
||||
icon={<FaTrash />}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default InitAndMaskUploadButtons;
|
217
frontend/src/features/options/OptionsAccordion.tsx
Normal file
@ -0,0 +1,217 @@
|
||||
import {
|
||||
Flex,
|
||||
Box,
|
||||
Text,
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionPanel,
|
||||
Switch,
|
||||
ExpandedIndex,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import { RootState } from '../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
|
||||
import {
|
||||
setShouldRunGFPGAN,
|
||||
setShouldRunESRGAN,
|
||||
OptionsState,
|
||||
setShouldUseInitImage,
|
||||
} from '../options/optionsSlice';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { setOpenAccordions, SystemState } from '../system/systemSlice';
|
||||
import SeedVariationOptions from './SeedVariationOptions';
|
||||
import SamplerOptions from './SamplerOptions';
|
||||
import ESRGANOptions from './ESRGANOptions';
|
||||
import GFPGANOptions from './GFPGANOptions';
|
||||
import OutputOptions from './OutputOptions';
|
||||
import ImageToImageOptions from './ImageToImageOptions';
|
||||
import { ChangeEvent } from 'react';
|
||||
|
||||
const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
return {
|
||||
initialImagePath: options.initialImagePath,
|
||||
shouldUseInitImage: options.shouldUseInitImage,
|
||||
shouldRunESRGAN: options.shouldRunESRGAN,
|
||||
shouldRunGFPGAN: options.shouldRunGFPGAN,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isGFPGANAvailable: system.isGFPGANAvailable,
|
||||
isESRGANAvailable: system.isESRGANAvailable,
|
||||
openAccordions: system.openAccordions,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Main container for generation and processing parameters.
|
||||
*/
|
||||
const OptionsAccordion = () => {
|
||||
const {
|
||||
shouldRunESRGAN,
|
||||
shouldRunGFPGAN,
|
||||
shouldUseInitImage,
|
||||
initialImagePath,
|
||||
} = useAppSelector(optionsSelector);
|
||||
|
||||
const { isGFPGANAvailable, isESRGANAvailable, openAccordions } =
|
||||
useAppSelector(systemSelector);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
/**
|
||||
* Stores accordion state in redux so preferred UI setup is retained.
|
||||
*/
|
||||
const handleChangeAccordionState = (openAccordions: ExpandedIndex) =>
|
||||
dispatch(setOpenAccordions(openAccordions));
|
||||
|
||||
const handleChangeShouldRunESRGAN = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldRunESRGAN(e.target.checked));
|
||||
|
||||
const handleChangeShouldRunGFPGAN = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldRunGFPGAN(e.target.checked));
|
||||
|
||||
const handleChangeShouldUseInitImage = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldUseInitImage(e.target.checked));
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
defaultIndex={openAccordions}
|
||||
allowMultiple
|
||||
reduceMotion
|
||||
onChange={handleChangeAccordionState}
|
||||
>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box flex="1" textAlign="left">
|
||||
Seed & Variation
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<SeedVariationOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box flex="1" textAlign="left">
|
||||
Sampler
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<SamplerOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Flex
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
width={'100%'}
|
||||
mr={2}
|
||||
>
|
||||
<Text>Upscale (ESRGAN)</Text>
|
||||
<Switch
|
||||
isDisabled={!isESRGANAvailable}
|
||||
isChecked={shouldRunESRGAN}
|
||||
onChange={handleChangeShouldRunESRGAN}
|
||||
/>
|
||||
</Flex>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<ESRGANOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Flex
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
width={'100%'}
|
||||
mr={2}
|
||||
>
|
||||
<Text>Fix Faces (GFPGAN)</Text>
|
||||
<Switch
|
||||
isDisabled={!isGFPGANAvailable}
|
||||
isChecked={shouldRunGFPGAN}
|
||||
onChange={handleChangeShouldRunGFPGAN}
|
||||
/>
|
||||
</Flex>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<GFPGANOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Flex
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
width={'100%'}
|
||||
mr={2}
|
||||
>
|
||||
<Text>Image to Image</Text>
|
||||
<Switch
|
||||
isDisabled={!initialImagePath}
|
||||
isChecked={shouldUseInitImage}
|
||||
onChange={handleChangeShouldUseInitImage}
|
||||
/>
|
||||
</Flex>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<ImageToImageOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box flex="1" textAlign="left">
|
||||
Output
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<OutputOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
export default OptionsAccordion;
|
76
frontend/src/features/options/OutputOptions.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
|
||||
import { RootState } from '../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
|
||||
import { setHeight, setWidth, setSeamless, OptionsState } from '../options/optionsSlice';
|
||||
|
||||
|
||||
import { HEIGHTS, WIDTHS } from '../../app/constants';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { ChangeEvent } from 'react';
|
||||
import SDSelect from '../../common/components/SDSelect';
|
||||
import SDSwitch from '../../common/components/SDSwitch';
|
||||
|
||||
const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
return {
|
||||
height: options.height,
|
||||
width: options.width,
|
||||
seamless: options.seamless,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Image output options. Includes width, height, seamless tiling.
|
||||
*/
|
||||
const OutputOptions = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { height, width, seamless } = useAppSelector(optionsSelector);
|
||||
|
||||
const handleChangeWidth = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||
dispatch(setWidth(Number(e.target.value)));
|
||||
|
||||
const handleChangeHeight = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||
dispatch(setHeight(Number(e.target.value)));
|
||||
|
||||
const handleChangeSeamless = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setSeamless(e.target.checked));
|
||||
|
||||
return (
|
||||
<Flex gap={2} direction={'column'}>
|
||||
<Flex gap={2}>
|
||||
<SDSelect
|
||||
label="Width"
|
||||
value={width}
|
||||
flexGrow={1}
|
||||
onChange={handleChangeWidth}
|
||||
validValues={WIDTHS}
|
||||
/>
|
||||
<SDSelect
|
||||
label="Height"
|
||||
value={height}
|
||||
flexGrow={1}
|
||||
onChange={handleChangeHeight}
|
||||
validValues={HEIGHTS}
|
||||
/>
|
||||
</Flex>
|
||||
<SDSwitch
|
||||
label="Seamless tiling"
|
||||
fontSize={'md'}
|
||||
isChecked={seamless}
|
||||
onChange={handleChangeSeamless}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default OutputOptions;
|
68
frontend/src/features/options/ProcessButtons.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { cancelProcessing, generateImage } from '../../app/socketio/actions';
|
||||
import { RootState } from '../../app/store';
|
||||
import SDButton from '../../common/components/SDButton';
|
||||
import useCheckParameters from '../../common/hooks/useCheckParameters';
|
||||
import { SystemState } from '../system/systemSlice';
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isProcessing: system.isProcessing,
|
||||
isConnected: system.isConnected,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Buttons to start and cancel image generation.
|
||||
*/
|
||||
const ProcessButtons = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { isProcessing, isConnected } = useAppSelector(systemSelector);
|
||||
const isReady = useCheckParameters();
|
||||
|
||||
const handleClickGenerate = () => dispatch(generateImage());
|
||||
|
||||
const handleClickCancel = () => dispatch(cancelProcessing());
|
||||
|
||||
return (
|
||||
<Flex
|
||||
gap={2}
|
||||
direction={'column'}
|
||||
alignItems={'space-between'}
|
||||
height={'100%'}
|
||||
>
|
||||
<SDButton
|
||||
label="Generate"
|
||||
type="submit"
|
||||
colorScheme="green"
|
||||
flexGrow={1}
|
||||
isDisabled={!isReady}
|
||||
fontSize={'md'}
|
||||
size={'md'}
|
||||
onClick={handleClickGenerate}
|
||||
/>
|
||||
<SDButton
|
||||
label="Cancel"
|
||||
colorScheme="red"
|
||||
flexGrow={1}
|
||||
fontSize={'md'}
|
||||
size={'md'}
|
||||
isDisabled={!isConnected || !isProcessing}
|
||||
onClick={handleClickCancel}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProcessButtons;
|
44
frontend/src/features/options/PromptInput.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { Textarea } from '@chakra-ui/react';
|
||||
import {
|
||||
ChangeEvent,
|
||||
KeyboardEvent,
|
||||
} from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { generateImage } from '../../app/socketio/actions';
|
||||
import { RootState } from '../../app/store';
|
||||
import { setPrompt } from '../options/optionsSlice';
|
||||
|
||||
/**
|
||||
* Prompt input text area.
|
||||
*/
|
||||
const PromptInput = () => {
|
||||
const { prompt } = useAppSelector((state: RootState) => state.options);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChangePrompt = (e: ChangeEvent<HTMLTextAreaElement>) =>
|
||||
dispatch(setPrompt(e.target.value));
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && e.shiftKey === false) {
|
||||
e.preventDefault();
|
||||
dispatch(generateImage())
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
id="prompt"
|
||||
name="prompt"
|
||||
resize="none"
|
||||
size={'lg'}
|
||||
height={'100%'}
|
||||
isInvalid={!prompt.length}
|
||||
onChange={handleChangePrompt}
|
||||
onKeyDown={handleKeyDown}
|
||||
value={prompt}
|
||||
placeholder="I'm dreaming of..."
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptInput;
|
74
frontend/src/features/options/SamplerOptions.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
|
||||
import { RootState } from '../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
|
||||
import { setCfgScale, setSampler, setSteps, OptionsState } from '../options/optionsSlice';
|
||||
|
||||
|
||||
import { SAMPLERS } from '../../app/constants';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { ChangeEvent } from 'react';
|
||||
import SDNumberInput from '../../common/components/SDNumberInput';
|
||||
import SDSelect from '../../common/components/SDSelect';
|
||||
|
||||
const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
return {
|
||||
steps: options.steps,
|
||||
cfgScale: options.cfgScale,
|
||||
sampler: options.sampler,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Sampler options. Includes steps, CFG scale, sampler.
|
||||
*/
|
||||
const SamplerOptions = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { steps, cfgScale, sampler } = useAppSelector(optionsSelector);
|
||||
|
||||
const handleChangeSteps = (v: string | number) =>
|
||||
dispatch(setSteps(Number(v)));
|
||||
|
||||
const handleChangeCfgScale = (v: string | number) =>
|
||||
dispatch(setCfgScale(Number(v)));
|
||||
|
||||
const handleChangeSampler = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||
dispatch(setSampler(e.target.value));
|
||||
|
||||
return (
|
||||
<Flex gap={2} direction={'column'}>
|
||||
<SDNumberInput
|
||||
label="Steps"
|
||||
min={1}
|
||||
step={1}
|
||||
precision={0}
|
||||
onChange={handleChangeSteps}
|
||||
value={steps}
|
||||
/>
|
||||
<SDNumberInput
|
||||
label="CFG scale"
|
||||
step={0.5}
|
||||
onChange={handleChangeCfgScale}
|
||||
value={cfgScale}
|
||||
/>
|
||||
<SDSelect
|
||||
label="Sampler"
|
||||
value={sampler}
|
||||
onChange={handleChangeSampler}
|
||||
validValues={SAMPLERS}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default SamplerOptions;
|
159
frontend/src/features/options/SeedVariationOptions.tsx
Normal file
@ -0,0 +1,159 @@
|
||||
import {
|
||||
Flex,
|
||||
Input,
|
||||
HStack,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Text,
|
||||
Button,
|
||||
} from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from '../../app/constants';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import SDNumberInput from '../../common/components/SDNumberInput';
|
||||
import SDSwitch from '../../common/components/SDSwitch';
|
||||
import randomInt from '../../common/util/randomInt';
|
||||
import { validateSeedWeights } from '../../common/util/seedWeightPairs';
|
||||
import {
|
||||
OptionsState,
|
||||
setIterations,
|
||||
setSeed,
|
||||
setSeedWeights,
|
||||
setShouldGenerateVariations,
|
||||
setShouldRandomizeSeed,
|
||||
setVariationAmount,
|
||||
} from './optionsSlice';
|
||||
|
||||
const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
return {
|
||||
variationAmount: options.variationAmount,
|
||||
seedWeights: options.seedWeights,
|
||||
shouldGenerateVariations: options.shouldGenerateVariations,
|
||||
shouldRandomizeSeed: options.shouldRandomizeSeed,
|
||||
seed: options.seed,
|
||||
iterations: options.iterations,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Seed & variation options. Includes iteration, seed, seed randomization, variation options.
|
||||
*/
|
||||
const SeedVariationOptions = () => {
|
||||
const {
|
||||
shouldGenerateVariations,
|
||||
variationAmount,
|
||||
seedWeights,
|
||||
shouldRandomizeSeed,
|
||||
seed,
|
||||
iterations,
|
||||
} = useAppSelector(optionsSelector);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChangeIterations = (v: string | number) =>
|
||||
dispatch(setIterations(Number(v)));
|
||||
|
||||
const handleChangeShouldRandomizeSeed = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldRandomizeSeed(e.target.checked));
|
||||
|
||||
const handleChangeSeed = (v: string | number) => dispatch(setSeed(Number(v)));
|
||||
|
||||
const handleClickRandomizeSeed = () =>
|
||||
dispatch(setSeed(randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX)));
|
||||
|
||||
const handleChangeShouldGenerateVariations = (
|
||||
e: ChangeEvent<HTMLInputElement>
|
||||
) => dispatch(setShouldGenerateVariations(e.target.checked));
|
||||
|
||||
const handleChangevariationAmount = (v: string | number) =>
|
||||
dispatch(setVariationAmount(Number(v)));
|
||||
|
||||
const handleChangeSeedWeights = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setSeedWeights(e.target.value));
|
||||
|
||||
return (
|
||||
<Flex gap={2} direction={'column'}>
|
||||
<SDNumberInput
|
||||
label="Images to generate"
|
||||
step={1}
|
||||
min={1}
|
||||
precision={0}
|
||||
onChange={handleChangeIterations}
|
||||
value={iterations}
|
||||
/>
|
||||
<SDSwitch
|
||||
label="Randomize seed on generation"
|
||||
isChecked={shouldRandomizeSeed}
|
||||
onChange={handleChangeShouldRandomizeSeed}
|
||||
/>
|
||||
<Flex gap={2}>
|
||||
<SDNumberInput
|
||||
label="Seed"
|
||||
step={1}
|
||||
precision={0}
|
||||
flexGrow={1}
|
||||
min={NUMPY_RAND_MIN}
|
||||
max={NUMPY_RAND_MAX}
|
||||
isDisabled={shouldRandomizeSeed}
|
||||
isInvalid={seed < 0 && shouldGenerateVariations}
|
||||
onChange={handleChangeSeed}
|
||||
value={seed}
|
||||
/>
|
||||
<Button
|
||||
size={'sm'}
|
||||
isDisabled={shouldRandomizeSeed}
|
||||
onClick={handleClickRandomizeSeed}
|
||||
>
|
||||
<Text pl={2} pr={2}>
|
||||
Shuffle
|
||||
</Text>
|
||||
</Button>
|
||||
</Flex>
|
||||
<SDSwitch
|
||||
label="Generate variations"
|
||||
isChecked={shouldGenerateVariations}
|
||||
width={'auto'}
|
||||
onChange={handleChangeShouldGenerateVariations}
|
||||
/>
|
||||
<SDNumberInput
|
||||
label="Variation amount"
|
||||
value={variationAmount}
|
||||
step={0.01}
|
||||
min={0}
|
||||
max={1}
|
||||
onChange={handleChangevariationAmount}
|
||||
/>
|
||||
<FormControl
|
||||
isInvalid={
|
||||
shouldGenerateVariations &&
|
||||
!(validateSeedWeights(seedWeights) || seedWeights === '')
|
||||
}
|
||||
flexGrow={1}
|
||||
>
|
||||
<HStack>
|
||||
<FormLabel marginInlineEnd={0} marginBottom={1}>
|
||||
<Text whiteSpace="nowrap">Seed Weights</Text>
|
||||
</FormLabel>
|
||||
<Input
|
||||
size={'sm'}
|
||||
value={seedWeights}
|
||||
onChange={handleChangeSeedWeights}
|
||||
/>
|
||||
</HStack>
|
||||
</FormControl>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default SeedVariationOptions;
|
@ -1,24 +1,15 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { SDMetadata } from '../gallery/gallerySlice';
|
||||
import randomInt from './util/randomInt';
|
||||
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from '../../app/constants';
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
import promptToString from '../../common/util/promptToString';
|
||||
import { seedWeightsToString } from '../../common/util/seedWeightPairs';
|
||||
|
||||
const calculateRealSteps = (
|
||||
steps: number,
|
||||
strength: number,
|
||||
hasInitImage: boolean
|
||||
): number => {
|
||||
return hasInitImage ? Math.floor(strength * steps) : steps;
|
||||
};
|
||||
export type UpscalingLevel = 2 | 4;
|
||||
|
||||
export type UpscalingLevel = 0 | 2 | 3 | 4;
|
||||
|
||||
export interface SDState {
|
||||
export interface OptionsState {
|
||||
prompt: string;
|
||||
iterations: number;
|
||||
steps: number;
|
||||
realSteps: number;
|
||||
cfgScale: number;
|
||||
height: number;
|
||||
width: number;
|
||||
@ -34,18 +25,17 @@ export interface SDState {
|
||||
seamless: boolean;
|
||||
shouldFitToWidthHeight: boolean;
|
||||
shouldGenerateVariations: boolean;
|
||||
variantAmount: number;
|
||||
variationAmount: number;
|
||||
seedWeights: string;
|
||||
shouldRunESRGAN: boolean;
|
||||
shouldRunGFPGAN: boolean;
|
||||
shouldRandomizeSeed: boolean;
|
||||
}
|
||||
|
||||
const initialSDState: SDState = {
|
||||
const initialOptionsState: OptionsState = {
|
||||
prompt: '',
|
||||
iterations: 1,
|
||||
steps: 50,
|
||||
realSteps: 50,
|
||||
cfgScale: 7.5,
|
||||
height: 512,
|
||||
width: 512,
|
||||
@ -58,7 +48,7 @@ const initialSDState: SDState = {
|
||||
maskPath: '',
|
||||
shouldFitToWidthHeight: true,
|
||||
shouldGenerateVariations: false,
|
||||
variantAmount: 0.1,
|
||||
variationAmount: 0.1,
|
||||
seedWeights: '',
|
||||
shouldRunESRGAN: false,
|
||||
upscalingLevel: 4,
|
||||
@ -68,27 +58,25 @@ const initialSDState: SDState = {
|
||||
shouldRandomizeSeed: true,
|
||||
};
|
||||
|
||||
const initialState: SDState = initialSDState;
|
||||
const initialState: OptionsState = initialOptionsState;
|
||||
|
||||
export const sdSlice = createSlice({
|
||||
name: 'sd',
|
||||
export const optionsSlice = createSlice({
|
||||
name: 'options',
|
||||
initialState,
|
||||
reducers: {
|
||||
setPrompt: (state, action: PayloadAction<string>) => {
|
||||
state.prompt = action.payload;
|
||||
setPrompt: (state, action: PayloadAction<string | InvokeAI.Prompt>) => {
|
||||
const newPrompt = action.payload;
|
||||
if (typeof newPrompt === 'string') {
|
||||
state.prompt = newPrompt;
|
||||
} else {
|
||||
state.prompt = promptToString(newPrompt);
|
||||
}
|
||||
},
|
||||
setIterations: (state, action: PayloadAction<number>) => {
|
||||
state.iterations = action.payload;
|
||||
},
|
||||
setSteps: (state, action: PayloadAction<number>) => {
|
||||
const { img2imgStrength, initialImagePath } = state;
|
||||
const steps = action.payload;
|
||||
state.steps = steps;
|
||||
state.realSteps = calculateRealSteps(
|
||||
steps,
|
||||
img2imgStrength,
|
||||
Boolean(initialImagePath)
|
||||
);
|
||||
state.steps = action.payload;
|
||||
},
|
||||
setCfgScale: (state, action: PayloadAction<number>) => {
|
||||
state.cfgScale = action.payload;
|
||||
@ -107,14 +95,7 @@ export const sdSlice = createSlice({
|
||||
state.shouldRandomizeSeed = false;
|
||||
},
|
||||
setImg2imgStrength: (state, action: PayloadAction<number>) => {
|
||||
const img2imgStrength = action.payload;
|
||||
const { steps, initialImagePath } = state;
|
||||
state.img2imgStrength = img2imgStrength;
|
||||
state.realSteps = calculateRealSteps(
|
||||
steps,
|
||||
img2imgStrength,
|
||||
Boolean(initialImagePath)
|
||||
);
|
||||
state.img2imgStrength = action.payload;
|
||||
},
|
||||
setGfpganStrength: (state, action: PayloadAction<number>) => {
|
||||
state.gfpganStrength = action.payload;
|
||||
@ -129,15 +110,9 @@ export const sdSlice = createSlice({
|
||||
state.shouldUseInitImage = action.payload;
|
||||
},
|
||||
setInitialImagePath: (state, action: PayloadAction<string>) => {
|
||||
const initialImagePath = action.payload;
|
||||
const { steps, img2imgStrength } = state;
|
||||
state.shouldUseInitImage = initialImagePath ? true : false;
|
||||
state.initialImagePath = initialImagePath;
|
||||
state.realSteps = calculateRealSteps(
|
||||
steps,
|
||||
img2imgStrength,
|
||||
Boolean(initialImagePath)
|
||||
);
|
||||
const newInitialImagePath = action.payload;
|
||||
state.shouldUseInitImage = newInitialImagePath ? true : false;
|
||||
state.initialImagePath = newInitialImagePath;
|
||||
},
|
||||
setMaskPath: (state, action: PayloadAction<string>) => {
|
||||
state.maskPath = action.payload;
|
||||
@ -151,13 +126,11 @@ export const sdSlice = createSlice({
|
||||
resetSeed: (state) => {
|
||||
state.seed = -1;
|
||||
},
|
||||
randomizeSeed: (state) => {
|
||||
state.seed = randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX);
|
||||
},
|
||||
setParameter: (
|
||||
state,
|
||||
action: PayloadAction<{ key: string; value: string | number | boolean }>
|
||||
) => {
|
||||
// TODO: This probably needs to be refactored.
|
||||
const { key, value } = action.payload;
|
||||
const temp = { ...state, [key]: value };
|
||||
if (key === 'seed') {
|
||||
@ -171,70 +144,95 @@ export const sdSlice = createSlice({
|
||||
setShouldGenerateVariations: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldGenerateVariations = action.payload;
|
||||
},
|
||||
setVariantAmount: (state, action: PayloadAction<number>) => {
|
||||
state.variantAmount = action.payload;
|
||||
setVariationAmount: (state, action: PayloadAction<number>) => {
|
||||
state.variationAmount = action.payload;
|
||||
},
|
||||
setSeedWeights: (state, action: PayloadAction<string>) => {
|
||||
state.seedWeights = action.payload;
|
||||
},
|
||||
setAllParameters: (state, action: PayloadAction<SDMetadata>) => {
|
||||
setAllParameters: (state, action: PayloadAction<InvokeAI.Metadata>) => {
|
||||
const {
|
||||
prompt,
|
||||
steps,
|
||||
cfgScale,
|
||||
height,
|
||||
width,
|
||||
type,
|
||||
postprocessing,
|
||||
sampler,
|
||||
prompt,
|
||||
seed,
|
||||
img2imgStrength,
|
||||
gfpganStrength,
|
||||
upscalingLevel,
|
||||
upscalingStrength,
|
||||
initialImagePath,
|
||||
maskPath,
|
||||
variations,
|
||||
steps,
|
||||
cfg_scale,
|
||||
seamless,
|
||||
shouldFitToWidthHeight,
|
||||
} = action.payload;
|
||||
width,
|
||||
height,
|
||||
strength,
|
||||
fit,
|
||||
init_image_path,
|
||||
mask_image_path,
|
||||
} = action.payload.image;
|
||||
|
||||
// ?? = falsy values ('', 0, etc) are used
|
||||
// || = falsy values not used
|
||||
state.prompt = prompt ?? state.prompt;
|
||||
state.steps = steps || state.steps;
|
||||
state.cfgScale = cfgScale || state.cfgScale;
|
||||
state.width = width || state.width;
|
||||
state.height = height || state.height;
|
||||
state.sampler = sampler || state.sampler;
|
||||
state.seed = seed ?? state.seed;
|
||||
state.seamless = seamless ?? state.seamless;
|
||||
state.shouldFitToWidthHeight =
|
||||
shouldFitToWidthHeight ?? state.shouldFitToWidthHeight;
|
||||
state.img2imgStrength = img2imgStrength ?? state.img2imgStrength;
|
||||
state.gfpganStrength = gfpganStrength ?? state.gfpganStrength;
|
||||
state.upscalingLevel = upscalingLevel ?? state.upscalingLevel;
|
||||
state.upscalingStrength = upscalingStrength ?? state.upscalingStrength;
|
||||
state.initialImagePath = initialImagePath ?? state.initialImagePath;
|
||||
state.maskPath = maskPath ?? state.maskPath;
|
||||
if (type === 'img2img') {
|
||||
if (init_image_path) state.initialImagePath = init_image_path;
|
||||
if (mask_image_path) state.maskPath = mask_image_path;
|
||||
if (strength) state.img2imgStrength = strength;
|
||||
if (typeof fit === 'boolean') state.shouldFitToWidthHeight = fit;
|
||||
state.shouldUseInitImage = true;
|
||||
} else {
|
||||
state.shouldUseInitImage = false;
|
||||
}
|
||||
|
||||
if (variations && variations.length > 0) {
|
||||
state.seedWeights = seedWeightsToString(variations);
|
||||
state.shouldGenerateVariations = true;
|
||||
} else {
|
||||
state.shouldGenerateVariations = false;
|
||||
}
|
||||
|
||||
// If the image whose parameters we are using has a seed, disable randomizing the seed
|
||||
if (seed) {
|
||||
state.seed = seed;
|
||||
state.shouldRandomizeSeed = false;
|
||||
}
|
||||
|
||||
// if we have a gfpgan strength, enable it
|
||||
state.shouldRunGFPGAN = gfpganStrength ? true : false;
|
||||
let postprocessingNotDone = ['gfpgan', 'esrgan'];
|
||||
if (postprocessing && postprocessing.length > 0) {
|
||||
postprocessing.forEach(
|
||||
(postprocess: InvokeAI.PostProcessedImageMetadata) => {
|
||||
if (postprocess.type === 'gfpgan') {
|
||||
const { strength } = postprocess;
|
||||
if (strength) state.gfpganStrength = strength;
|
||||
state.shouldRunGFPGAN = true;
|
||||
postprocessingNotDone = postprocessingNotDone.filter(
|
||||
(p) => p !== 'gfpgan'
|
||||
);
|
||||
}
|
||||
if (postprocess.type === 'esrgan') {
|
||||
const { scale, strength } = postprocess;
|
||||
if (scale) state.upscalingLevel = scale;
|
||||
if (strength) state.upscalingStrength = strength;
|
||||
state.shouldRunESRGAN = true;
|
||||
postprocessingNotDone = postprocessingNotDone.filter(
|
||||
(p) => p !== 'esrgan'
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// if we have a esrgan strength, enable it
|
||||
state.shouldRunESRGAN = upscalingLevel ? true : false;
|
||||
postprocessingNotDone.forEach((p) => {
|
||||
if (p === 'esrgan') state.shouldRunESRGAN = false;
|
||||
if (p === 'gfpgan') state.shouldRunGFPGAN = false;
|
||||
});
|
||||
|
||||
// if we want to recreate an image exactly, we disable variations
|
||||
state.shouldGenerateVariations = false;
|
||||
|
||||
state.shouldUseInitImage = initialImagePath ? true : false;
|
||||
if (prompt) state.prompt = promptToString(prompt);
|
||||
if (sampler) state.sampler = sampler;
|
||||
if (steps) state.steps = steps;
|
||||
if (cfg_scale) state.cfgScale = cfg_scale;
|
||||
if (typeof seamless === 'boolean') state.seamless = seamless;
|
||||
if (width) state.width = width;
|
||||
if (height) state.height = height;
|
||||
},
|
||||
resetSDState: (state) => {
|
||||
resetOptionsState: (state) => {
|
||||
return {
|
||||
...state,
|
||||
...initialSDState,
|
||||
...initialOptionsState,
|
||||
};
|
||||
},
|
||||
setShouldRunGFPGAN: (state, action: PayloadAction<boolean>) => {
|
||||
@ -267,17 +265,16 @@ export const {
|
||||
setInitialImagePath,
|
||||
setMaskPath,
|
||||
resetSeed,
|
||||
randomizeSeed,
|
||||
resetSDState,
|
||||
resetOptionsState,
|
||||
setShouldFitToWidthHeight,
|
||||
setParameter,
|
||||
setShouldGenerateVariations,
|
||||
setSeedWeights,
|
||||
setVariantAmount,
|
||||
setVariationAmount,
|
||||
setAllParameters,
|
||||
setShouldRunGFPGAN,
|
||||
setShouldRunESRGAN,
|
||||
setShouldRandomizeSeed,
|
||||
} = sdSlice.actions;
|
||||
} = optionsSlice.actions;
|
||||
|
||||
export default sdSlice.reducer;
|
||||
export default optionsSlice.reducer;
|
@ -1,84 +0,0 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
|
||||
import { RootState } from '../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
|
||||
import {
|
||||
setUpscalingLevel,
|
||||
setUpscalingStrength,
|
||||
UpscalingLevel,
|
||||
SDState,
|
||||
} from '../sd/sdSlice';
|
||||
|
||||
import SDNumberInput from '../../components/SDNumberInput';
|
||||
import SDSelect from '../../components/SDSelect';
|
||||
|
||||
import { UPSCALING_LEVELS } from '../../app/constants';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { SystemState } from '../system/systemSlice';
|
||||
|
||||
const sdSelector = createSelector(
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
upscalingLevel: sd.upscalingLevel,
|
||||
upscalingStrength: sd.upscalingStrength,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isESRGANAvailable: system.isESRGANAvailable,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
const ESRGANOptions = () => {
|
||||
const { upscalingLevel, upscalingStrength } = useAppSelector(sdSelector);
|
||||
|
||||
const { isESRGANAvailable } = useAppSelector(systemSelector);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
return (
|
||||
<Flex direction={'column'} gap={2}>
|
||||
<SDSelect
|
||||
isDisabled={!isESRGANAvailable}
|
||||
label='Scale'
|
||||
value={upscalingLevel}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
setUpscalingLevel(
|
||||
Number(e.target.value) as UpscalingLevel
|
||||
)
|
||||
)
|
||||
}
|
||||
validValues={UPSCALING_LEVELS}
|
||||
/>
|
||||
<SDNumberInput
|
||||
isDisabled={!isESRGANAvailable}
|
||||
label='Strength'
|
||||
step={0.05}
|
||||
min={0}
|
||||
max={1}
|
||||
onChange={(v) => dispatch(setUpscalingStrength(Number(v)))}
|
||||
value={upscalingStrength}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ESRGANOptions;
|
@ -1,63 +0,0 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
|
||||
import { RootState } from '../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
|
||||
import { SDState, setGfpganStrength } from '../sd/sdSlice';
|
||||
|
||||
import SDNumberInput from '../../components/SDNumberInput';
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { SystemState } from '../system/systemSlice';
|
||||
|
||||
const sdSelector = createSelector(
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
gfpganStrength: sd.gfpganStrength,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isGFPGANAvailable: system.isGFPGANAvailable,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
const GFPGANOptions = () => {
|
||||
const { gfpganStrength } = useAppSelector(sdSelector);
|
||||
|
||||
const { isGFPGANAvailable } = useAppSelector(systemSelector);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
return (
|
||||
<Flex direction={'column'} gap={2}>
|
||||
<SDNumberInput
|
||||
isDisabled={!isGFPGANAvailable}
|
||||
label='Strength'
|
||||
step={0.05}
|
||||
min={0}
|
||||
max={1}
|
||||
onChange={(v) => dispatch(setGfpganStrength(Number(v)))}
|
||||
value={gfpganStrength}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default GFPGANOptions;
|
@ -1,54 +0,0 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
import { RootState } from '../../app/store';
|
||||
import SDNumberInput from '../../components/SDNumberInput';
|
||||
import SDSwitch from '../../components/SDSwitch';
|
||||
import InitImage from './InitImage';
|
||||
import {
|
||||
SDState,
|
||||
setImg2imgStrength,
|
||||
setShouldFitToWidthHeight,
|
||||
} from './sdSlice';
|
||||
|
||||
const sdSelector = createSelector(
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
initialImagePath: sd.initialImagePath,
|
||||
img2imgStrength: sd.img2imgStrength,
|
||||
shouldFitToWidthHeight: sd.shouldFitToWidthHeight,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const ImageToImageOptions = () => {
|
||||
const { initialImagePath, img2imgStrength, shouldFitToWidthHeight } =
|
||||
useAppSelector(sdSelector);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
return (
|
||||
<Flex direction={'column'} gap={2}>
|
||||
<SDNumberInput
|
||||
isDisabled={!initialImagePath}
|
||||
label='Strength'
|
||||
step={0.01}
|
||||
min={0}
|
||||
max={1}
|
||||
onChange={(v) => dispatch(setImg2imgStrength(Number(v)))}
|
||||
value={img2imgStrength}
|
||||
/>
|
||||
<SDSwitch
|
||||
isDisabled={!initialImagePath}
|
||||
label='Fit initial image to output size'
|
||||
isChecked={shouldFitToWidthHeight}
|
||||
onChange={(e) =>
|
||||
dispatch(setShouldFitToWidthHeight(e.target.checked))
|
||||
}
|
||||
/>
|
||||
<InitImage />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageToImageOptions;
|
@ -1,155 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
IconButton,
|
||||
Image,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { SyntheticEvent, useCallback, useState } from 'react';
|
||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
||||
import { FaTrash } from 'react-icons/fa';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
import { RootState } from '../../app/store';
|
||||
import {
|
||||
SDState,
|
||||
setInitialImagePath,
|
||||
setMaskPath,
|
||||
} from '../../features/sd/sdSlice';
|
||||
import MaskUploader from './MaskUploader';
|
||||
import './InitImage.css';
|
||||
import { uploadInitialImage } from '../../app/socketio';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
const sdSelector = createSelector(
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
initialImagePath: sd.initialImagePath,
|
||||
maskPath: sd.maskPath,
|
||||
};
|
||||
},
|
||||
{ memoizeOptions: { resultEqualityCheck: isEqual } }
|
||||
);
|
||||
|
||||
const InitImage = () => {
|
||||
const toast = useToast();
|
||||
const dispatch = useAppDispatch();
|
||||
const { initialImagePath, maskPath } = useAppSelector(sdSelector);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: Array<File>, fileRejections: Array<FileRejection>) => {
|
||||
fileRejections.forEach((rejection: FileRejection) => {
|
||||
const msg = rejection.errors.reduce(
|
||||
(acc: string, cur: { message: string }) => acc + '\n' + cur.message,
|
||||
''
|
||||
);
|
||||
|
||||
toast({
|
||||
title: 'Upload failed',
|
||||
description: msg,
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
});
|
||||
});
|
||||
|
||||
acceptedFiles.forEach((file: File) => {
|
||||
dispatch(uploadInitialImage(file));
|
||||
});
|
||||
},
|
||||
[dispatch, toast]
|
||||
);
|
||||
|
||||
const { getRootProps, getInputProps, open } = useDropzone({
|
||||
onDrop,
|
||||
accept: {
|
||||
'image/jpeg': ['.jpg', '.jpeg', '.png'],
|
||||
},
|
||||
});
|
||||
|
||||
const [shouldShowMask, setShouldShowMask] = useState<boolean>(false);
|
||||
const handleClickUploadIcon = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
open();
|
||||
};
|
||||
const handleClickResetInitialImageAndMask = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
dispatch(setInitialImagePath(''));
|
||||
dispatch(setMaskPath(''));
|
||||
};
|
||||
|
||||
const handleMouseOverInitialImageUploadButton = () =>
|
||||
setShouldShowMask(false);
|
||||
const handleMouseOutInitialImageUploadButton = () => setShouldShowMask(true);
|
||||
|
||||
const handleMouseOverMaskUploadButton = () => setShouldShowMask(true);
|
||||
const handleMouseOutMaskUploadButton = () => setShouldShowMask(true);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
{...getRootProps({
|
||||
onClick: initialImagePath ? (e) => e.stopPropagation() : undefined,
|
||||
})}
|
||||
direction={'column'}
|
||||
alignItems={'center'}
|
||||
gap={2}
|
||||
>
|
||||
<input {...getInputProps({ multiple: false })} />
|
||||
<Flex gap={2} justifyContent={'space-between'} width={'100%'}>
|
||||
<Button
|
||||
size={'sm'}
|
||||
fontSize={'md'}
|
||||
fontWeight={'normal'}
|
||||
onClick={handleClickUploadIcon}
|
||||
onMouseOver={handleMouseOverInitialImageUploadButton}
|
||||
onMouseOut={handleMouseOutInitialImageUploadButton}
|
||||
>
|
||||
Upload Image
|
||||
</Button>
|
||||
|
||||
<MaskUploader>
|
||||
<Button
|
||||
size={'sm'}
|
||||
fontSize={'md'}
|
||||
fontWeight={'normal'}
|
||||
onClick={handleClickUploadIcon}
|
||||
onMouseOver={handleMouseOverMaskUploadButton}
|
||||
onMouseOut={handleMouseOutMaskUploadButton}
|
||||
>
|
||||
Upload Mask
|
||||
</Button>
|
||||
</MaskUploader>
|
||||
<IconButton
|
||||
size={'sm'}
|
||||
aria-label={'Reset initial image and mask'}
|
||||
onClick={handleClickResetInitialImageAndMask}
|
||||
icon={<FaTrash />}
|
||||
/>
|
||||
</Flex>
|
||||
{initialImagePath && (
|
||||
<Flex position={'relative'} width={'100%'}>
|
||||
<Image
|
||||
fit={'contain'}
|
||||
src={initialImagePath}
|
||||
rounded={'md'}
|
||||
className={'checkerboard'}
|
||||
/>
|
||||
{shouldShowMask && maskPath && (
|
||||
<Image
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
fit={'contain'}
|
||||
src={maskPath}
|
||||
rounded={'md'}
|
||||
zIndex={1}
|
||||
className={'checkerboard'}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default InitImage;
|
@ -1,61 +0,0 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { cloneElement, ReactElement, SyntheticEvent, useCallback } from 'react';
|
||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
||||
import { useAppDispatch } from '../../app/hooks';
|
||||
import { uploadMaskImage } from '../../app/socketio';
|
||||
|
||||
type Props = {
|
||||
children: ReactElement;
|
||||
};
|
||||
|
||||
const MaskUploader = ({ children }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const toast = useToast();
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: Array<File>, fileRejections: Array<FileRejection>) => {
|
||||
fileRejections.forEach((rejection: FileRejection) => {
|
||||
const msg = rejection.errors.reduce(
|
||||
(acc: string, cur: { message: string }) =>
|
||||
acc + '\n' + cur.message,
|
||||
''
|
||||
);
|
||||
|
||||
toast({
|
||||
title: 'Upload failed',
|
||||
description: msg,
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
});
|
||||
});
|
||||
|
||||
acceptedFiles.forEach((file: File) => {
|
||||
dispatch(uploadMaskImage(file));
|
||||
});
|
||||
},
|
||||
[dispatch, toast]
|
||||
);
|
||||
|
||||
const { getRootProps, getInputProps, open } = useDropzone({
|
||||
onDrop,
|
||||
accept: {
|
||||
'image/jpeg': ['.jpg', '.jpeg', '.png'],
|
||||
},
|
||||
});
|
||||
|
||||
const handleClickUploadIcon = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
open();
|
||||
};
|
||||
|
||||
return (
|
||||
<div {...getRootProps()}>
|
||||
<input {...getInputProps({ multiple: false })} />
|
||||
{cloneElement(children, {
|
||||
onClick: handleClickUploadIcon,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MaskUploader;
|
@ -1,211 +0,0 @@
|
||||
import {
|
||||
Flex,
|
||||
Box,
|
||||
Text,
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionPanel,
|
||||
Switch,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import { RootState } from '../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
|
||||
import {
|
||||
setShouldRunGFPGAN,
|
||||
setShouldRunESRGAN,
|
||||
SDState,
|
||||
setShouldUseInitImage,
|
||||
} from '../sd/sdSlice';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { setOpenAccordions, SystemState } from '../system/systemSlice';
|
||||
import SeedVariationOptions from './SeedVariationOptions';
|
||||
import SamplerOptions from './SamplerOptions';
|
||||
import ESRGANOptions from './ESRGANOptions';
|
||||
import GFPGANOptions from './GFPGANOptions';
|
||||
import OutputOptions from './OutputOptions';
|
||||
import ImageToImageOptions from './ImageToImageOptions';
|
||||
|
||||
const sdSelector = createSelector(
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
initialImagePath: sd.initialImagePath,
|
||||
shouldUseInitImage: sd.shouldUseInitImage,
|
||||
shouldRunESRGAN: sd.shouldRunESRGAN,
|
||||
shouldRunGFPGAN: sd.shouldRunGFPGAN,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isGFPGANAvailable: system.isGFPGANAvailable,
|
||||
isESRGANAvailable: system.isESRGANAvailable,
|
||||
openAccordions: system.openAccordions,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const OptionsAccordion = () => {
|
||||
const {
|
||||
shouldRunESRGAN,
|
||||
shouldRunGFPGAN,
|
||||
shouldUseInitImage,
|
||||
initialImagePath,
|
||||
} = useAppSelector(sdSelector);
|
||||
|
||||
const { isGFPGANAvailable, isESRGANAvailable, openAccordions } =
|
||||
useAppSelector(systemSelector);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
defaultIndex={openAccordions}
|
||||
allowMultiple
|
||||
reduceMotion
|
||||
onChange={(openAccordions) =>
|
||||
dispatch(setOpenAccordions(openAccordions))
|
||||
}
|
||||
>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box flex='1' textAlign='left'>
|
||||
Seed & Variation
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<SeedVariationOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box flex='1' textAlign='left'>
|
||||
Sampler
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<SamplerOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Flex
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
width={'100%'}
|
||||
mr={2}
|
||||
>
|
||||
<Text>Upscale (ESRGAN)</Text>
|
||||
<Switch
|
||||
isDisabled={!isESRGANAvailable}
|
||||
isChecked={shouldRunESRGAN}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
setShouldRunESRGAN(e.target.checked)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<ESRGANOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Flex
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
width={'100%'}
|
||||
mr={2}
|
||||
>
|
||||
<Text>Fix Faces (GFPGAN)</Text>
|
||||
<Switch
|
||||
isDisabled={!isGFPGANAvailable}
|
||||
isChecked={shouldRunGFPGAN}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
setShouldRunGFPGAN(e.target.checked)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<GFPGANOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Flex
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
width={'100%'}
|
||||
mr={2}
|
||||
>
|
||||
<Text>Image to Image</Text>
|
||||
<Switch
|
||||
isDisabled={!initialImagePath}
|
||||
isChecked={shouldUseInitImage}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
setShouldUseInitImage(e.target.checked)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<ImageToImageOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box flex='1' textAlign='left'>
|
||||
Output
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<OutputOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
export default OptionsAccordion;
|