mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge remote-tracking branch 'upstream/development' into development
This commit is contained in:
commit
8c72da3643
@ -7,6 +7,8 @@ import eventlet
|
|||||||
import glob
|
import glob
|
||||||
import shlex
|
import shlex
|
||||||
import argparse
|
import argparse
|
||||||
|
import math
|
||||||
|
import shutil
|
||||||
|
|
||||||
from flask_socketio import SocketIO
|
from flask_socketio import SocketIO
|
||||||
from flask import Flask, send_from_directory, url_for, jsonify
|
from flask import Flask, send_from_directory, url_for, jsonify
|
||||||
@ -15,6 +17,7 @@ from PIL import Image
|
|||||||
from pytorch_lightning import logging
|
from pytorch_lightning import logging
|
||||||
from threading import Event
|
from threading import Event
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
from send2trash import send2trash
|
||||||
|
|
||||||
from ldm.gfpgan.gfpgan_tools import real_esrgan_upscale
|
from ldm.gfpgan.gfpgan_tools import real_esrgan_upscale
|
||||||
from ldm.gfpgan.gfpgan_tools import run_gfpgan
|
from ldm.gfpgan.gfpgan_tools import run_gfpgan
|
||||||
@ -118,15 +121,15 @@ result_path = os.path.join(output_dir, 'img-samples/')
|
|||||||
intermediate_path = os.path.join(result_path, 'intermediates/')
|
intermediate_path = os.path.join(result_path, 'intermediates/')
|
||||||
|
|
||||||
# path for user-uploaded init images and masks
|
# path for user-uploaded init images and masks
|
||||||
init_path = os.path.join(result_path, 'init-images/')
|
init_image_path = os.path.join(result_path, 'init-images/')
|
||||||
mask_path = os.path.join(result_path, 'mask-images/')
|
mask_image_path = os.path.join(result_path, 'mask-images/')
|
||||||
|
|
||||||
# txt log
|
# 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
|
# make all output paths
|
||||||
[os.makedirs(path, exist_ok=True)
|
[os.makedirs(path, exist_ok=True)
|
||||||
for path in [result_path, intermediate_path, init_path, mask_path]]
|
for path in [result_path, intermediate_path, init_image_path, mask_image_path]]
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -154,7 +157,8 @@ def handle_request_all_images():
|
|||||||
else:
|
else:
|
||||||
metadata = all_metadata['sd-metadata']
|
metadata = all_metadata['sd-metadata']
|
||||||
image_array.append({'path': path, 'metadata': metadata})
|
image_array.append({'path': path, 'metadata': metadata})
|
||||||
return make_response("OK", data=image_array)
|
socketio.emit('galleryImages', {'images': image_array})
|
||||||
|
eventlet.sleep(0)
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('generateImage')
|
@socketio.on('generateImage')
|
||||||
@ -165,16 +169,32 @@ def handle_generate_image_event(generation_parameters, esrgan_parameters, gfpgan
|
|||||||
esrgan_parameters,
|
esrgan_parameters,
|
||||||
gfpgan_parameters
|
gfpgan_parameters
|
||||||
)
|
)
|
||||||
return make_response("OK")
|
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('runESRGAN')
|
@socketio.on('runESRGAN')
|
||||||
def handle_run_esrgan_event(original_image, esrgan_parameters):
|
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"])
|
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 = real_esrgan_upscale(
|
||||||
image=image,
|
image=image,
|
||||||
upsampler_scale=esrgan_parameters['upscale'][0],
|
upsampler_scale=esrgan_parameters['upscale'][0],
|
||||||
@ -182,24 +202,54 @@ def handle_run_esrgan_event(original_image, esrgan_parameters):
|
|||||||
seed=seed
|
seed=seed
|
||||||
)
|
)
|
||||||
|
|
||||||
|
progress['currentStatus'] = 'Saving image'
|
||||||
|
socketio.emit('progressUpdate', progress)
|
||||||
|
eventlet.sleep(0)
|
||||||
|
|
||||||
esrgan_parameters['seed'] = seed
|
esrgan_parameters['seed'] = seed
|
||||||
path = save_image(image, esrgan_parameters, result_path, postprocessing='esrgan')
|
path = save_image(image, esrgan_parameters, result_path, postprocessing='esrgan')
|
||||||
command = parameters_to_command(esrgan_parameters)
|
command = parameters_to_command(esrgan_parameters)
|
||||||
|
|
||||||
write_log_message(f'[Upscaled] "{original_image["url"]}" > "{path}": {command}')
|
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(
|
socketio.emit(
|
||||||
'result', {'url': os.path.relpath(path), 'type': 'esrgan', 'uuid': original_image['uuid'],'metadata': esrgan_parameters})
|
'esrganResult', {'url': os.path.relpath(path), 'uuid': original_image['uuid'], 'metadata': esrgan_parameters})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('runGFPGAN')
|
@socketio.on('runGFPGAN')
|
||||||
def handle_run_gfpgan_event(original_image, gfpgan_parameters):
|
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"])
|
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 = run_gfpgan(
|
||||||
image=image,
|
image=image,
|
||||||
strength=gfpgan_parameters['gfpgan_strength'],
|
strength=gfpgan_parameters['gfpgan_strength'],
|
||||||
@ -207,29 +257,42 @@ def handle_run_gfpgan_event(original_image, gfpgan_parameters):
|
|||||||
upsampler_scale=1
|
upsampler_scale=1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
progress['currentStatus'] = 'Saving image'
|
||||||
|
socketio.emit('progressUpdate', progress)
|
||||||
|
eventlet.sleep(0)
|
||||||
|
|
||||||
gfpgan_parameters['seed'] = seed
|
gfpgan_parameters['seed'] = seed
|
||||||
path = save_image(image, gfpgan_parameters, result_path, postprocessing='gfpgan')
|
path = save_image(image, gfpgan_parameters, result_path, postprocessing='gfpgan')
|
||||||
command = parameters_to_command(gfpgan_parameters)
|
command = parameters_to_command(gfpgan_parameters)
|
||||||
|
|
||||||
write_log_message(f'[Fixed faces] "{original_image["url"]}" > "{path}": {command}')
|
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(
|
socketio.emit(
|
||||||
'result', {'url': os.path.relpath(path), 'type': 'gfpgan', 'uuid': original_image['uuid'],'metadata': gfpgan_parameters})
|
'gfpganResult', {'url': os.path.relpath(path), 'uuid': original_image['uuid'], 'metadata': gfpgan_parameters})
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('cancel')
|
@socketio.on('cancel')
|
||||||
def handle_cancel():
|
def handle_cancel():
|
||||||
print(f'>> Cancel processing requested')
|
print(f'>> Cancel processing requested')
|
||||||
canceled.set()
|
canceled.set()
|
||||||
return make_response("OK")
|
socketio.emit('processingCanceled')
|
||||||
|
|
||||||
|
|
||||||
# TODO: I think this needs a safety mechanism.
|
# TODO: I think this needs a safety mechanism.
|
||||||
@socketio.on('deleteImage')
|
@socketio.on('deleteImage')
|
||||||
def handle_delete_image(path):
|
def handle_delete_image(path, uuid):
|
||||||
print(f'>> Delete requested "{path}"')
|
print(f'>> Delete requested "{path}"')
|
||||||
Path(path).unlink()
|
send2trash(path)
|
||||||
return make_response("OK")
|
socketio.emit('imageDeleted', {'url': path, 'uuid': uuid})
|
||||||
|
|
||||||
|
|
||||||
# TODO: I think this needs a safety mechanism.
|
# TODO: I think this needs a safety mechanism.
|
||||||
@ -239,11 +302,11 @@ def handle_upload_initial_image(bytes, name):
|
|||||||
uuid = uuid4().hex
|
uuid = uuid4().hex
|
||||||
split = os.path.splitext(name)
|
split = os.path.splitext(name)
|
||||||
name = f'{split[0]}.{uuid}{split[1]}'
|
name = f'{split[0]}.{uuid}{split[1]}'
|
||||||
file_path = os.path.join(init_path, name)
|
file_path = os.path.join(init_image_path, name)
|
||||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||||
newFile = open(file_path, "wb")
|
newFile = open(file_path, "wb")
|
||||||
newFile.write(bytes)
|
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.
|
# TODO: I think this needs a safety mechanism.
|
||||||
@ -253,11 +316,11 @@ def handle_upload_mask_image(bytes, name):
|
|||||||
uuid = uuid4().hex
|
uuid = uuid4().hex
|
||||||
split = os.path.splitext(name)
|
split = os.path.splitext(name)
|
||||||
name = f'{split[0]}.{uuid}{split[1]}'
|
name = f'{split[0]}.{uuid}{split[1]}'
|
||||||
file_path = os.path.join(mask_path, name)
|
file_path = os.path.join(mask_image_path, name)
|
||||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||||
newFile = open(file_path, "wb")
|
newFile = open(file_path, "wb")
|
||||||
newFile.write(bytes)
|
newFile.write(bytes)
|
||||||
return make_response("OK", data=file_path)
|
socketio.emit('maskImageUploaded', {'url': file_path, 'uuid': ''})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -272,6 +335,13 @@ ADDITIONAL FUNCTIONS
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
def write_log_message(message, log_path=log_path):
|
||||||
"""Logs the filename and parameters used to generate or process that image to log file"""
|
"""Logs the filename and parameters used to generate or process that image to log file"""
|
||||||
message = f'{message}\n'
|
message = f'{message}\n'
|
||||||
@ -279,15 +349,6 @@ def write_log_message(message, log_path=log_path):
|
|||||||
file.writelines(message)
|
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):
|
def save_image(image, parameters, output_dir, step_index=None, postprocessing=False):
|
||||||
seed = parameters['seed'] if 'seed' in parameters else 'unknown_seed'
|
seed = parameters['seed'] if 'seed' in parameters else 'unknown_seed'
|
||||||
|
|
||||||
@ -309,16 +370,69 @@ def save_image(image, parameters, output_dir, step_index=None, postprocessing=Fa
|
|||||||
|
|
||||||
return path
|
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):
|
def generate_images(generation_parameters, esrgan_parameters, gfpgan_parameters):
|
||||||
canceled.clear()
|
canceled.clear()
|
||||||
|
|
||||||
step_index = 1
|
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):
|
def image_progress(sample, step):
|
||||||
if canceled.is_set():
|
if canceled.is_set():
|
||||||
raise CanceledException
|
raise CanceledException
|
||||||
|
|
||||||
nonlocal step_index
|
nonlocal step_index
|
||||||
nonlocal generation_parameters
|
nonlocal generation_parameters
|
||||||
|
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:
|
if generation_parameters["progress_images"] and step % 5 == 0 and step < generation_parameters['steps'] - 1:
|
||||||
image = model.sample_to_image(sample)
|
image = model.sample_to_image(sample)
|
||||||
path = save_image(image, generation_parameters, intermediate_path, step_index)
|
path = save_image(image, generation_parameters, intermediate_path, step_index)
|
||||||
@ -326,18 +440,30 @@ def generate_images(generation_parameters, esrgan_parameters, gfpgan_parameters)
|
|||||||
step_index += 1
|
step_index += 1
|
||||||
socketio.emit('intermediateResult', {
|
socketio.emit('intermediateResult', {
|
||||||
'url': os.path.relpath(path), 'metadata': generation_parameters})
|
'url': os.path.relpath(path), 'metadata': generation_parameters})
|
||||||
socketio.emit('progress', {'step': step + 1})
|
socketio.emit('progressUpdate', progress)
|
||||||
eventlet.sleep(0)
|
eventlet.sleep(0)
|
||||||
|
|
||||||
def image_done(image, seed):
|
def image_done(image, seed):
|
||||||
nonlocal generation_parameters
|
nonlocal generation_parameters
|
||||||
nonlocal esrgan_parameters
|
nonlocal esrgan_parameters
|
||||||
nonlocal gfpgan_parameters
|
nonlocal gfpgan_parameters
|
||||||
|
nonlocal progress
|
||||||
|
|
||||||
|
step_index = 1
|
||||||
|
|
||||||
|
progress['currentStatus'] = 'Generation complete'
|
||||||
|
socketio.emit('progressUpdate', progress)
|
||||||
|
eventlet.sleep(0)
|
||||||
|
|
||||||
all_parameters = generation_parameters
|
all_parameters = generation_parameters
|
||||||
postprocessing = False
|
postprocessing = False
|
||||||
|
|
||||||
if esrgan_parameters:
|
if esrgan_parameters:
|
||||||
|
progress['currentStatus'] = 'Upscaling'
|
||||||
|
progress['currentStatusHasSteps'] = False
|
||||||
|
socketio.emit('progressUpdate', progress)
|
||||||
|
eventlet.sleep(0)
|
||||||
|
|
||||||
image = real_esrgan_upscale(
|
image = real_esrgan_upscale(
|
||||||
image=image,
|
image=image,
|
||||||
strength=esrgan_parameters['strength'],
|
strength=esrgan_parameters['strength'],
|
||||||
@ -348,6 +474,11 @@ def generate_images(generation_parameters, esrgan_parameters, gfpgan_parameters)
|
|||||||
all_parameters["upscale"] = [esrgan_parameters['level'], esrgan_parameters['strength']]
|
all_parameters["upscale"] = [esrgan_parameters['level'], esrgan_parameters['strength']]
|
||||||
|
|
||||||
if gfpgan_parameters:
|
if gfpgan_parameters:
|
||||||
|
progress['currentStatus'] = 'Fixing faces'
|
||||||
|
progress['currentStatusHasSteps'] = False
|
||||||
|
socketio.emit('progressUpdate', progress)
|
||||||
|
eventlet.sleep(0)
|
||||||
|
|
||||||
image = run_gfpgan(
|
image = run_gfpgan(
|
||||||
image=image,
|
image=image,
|
||||||
strength=gfpgan_parameters['strength'],
|
strength=gfpgan_parameters['strength'],
|
||||||
@ -358,6 +489,9 @@ def generate_images(generation_parameters, esrgan_parameters, gfpgan_parameters)
|
|||||||
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)
|
path = save_image(image, all_parameters, result_path, postprocessing=postprocessing)
|
||||||
command = parameters_to_command(all_parameters)
|
command = parameters_to_command(all_parameters)
|
||||||
@ -365,8 +499,24 @@ def generate_images(generation_parameters, esrgan_parameters, gfpgan_parameters)
|
|||||||
print(f'Image generated: "{path}"')
|
print(f'Image generated: "{path}"')
|
||||||
write_log_message(f'[Generated] "{path}": {command}')
|
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(
|
socketio.emit(
|
||||||
'result', {'url': os.path.relpath(path), 'type': 'generation', 'metadata': all_parameters})
|
'generationResult', {'url': os.path.relpath(path), 'metadata': all_parameters})
|
||||||
eventlet.sleep(0)
|
eventlet.sleep(0)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -381,7 +531,7 @@ def generate_images(generation_parameters, esrgan_parameters, gfpgan_parameters)
|
|||||||
except CanceledException:
|
except CanceledException:
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
socketio.emit('error', (str(e)))
|
socketio.emit('error', {'message': (str(e))})
|
||||||
print("\n")
|
print("\n")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
print("\n")
|
print("\n")
|
||||||
|
@ -48,6 +48,7 @@ dependencies:
|
|||||||
- opencv-python==4.6.0
|
- opencv-python==4.6.0
|
||||||
- protobuf==3.20.1
|
- protobuf==3.20.1
|
||||||
- realesrgan==0.2.5.0
|
- realesrgan==0.2.5.0
|
||||||
|
- send2trash==1.8.0
|
||||||
- test-tube==0.7.5
|
- test-tube==0.7.5
|
||||||
- transformers==4.21.2
|
- transformers==4.21.2
|
||||||
- torch-fidelity==0.3.0
|
- torch-fidelity==0.3.0
|
||||||
|
@ -20,6 +20,7 @@ dependencies:
|
|||||||
- realesrgan==0.2.5.0
|
- realesrgan==0.2.5.0
|
||||||
- test-tube>=0.7.5
|
- test-tube>=0.7.5
|
||||||
- streamlit==1.12.0
|
- streamlit==1.12.0
|
||||||
|
- send2trash==1.8.0
|
||||||
- pillow==9.2.0
|
- pillow==9.2.0
|
||||||
- einops==0.3.0
|
- einops==0.3.0
|
||||||
- torch-fidelity==0.3.0
|
- torch-fidelity==0.3.0
|
||||||
|
@ -1,85 +1,37 @@
|
|||||||
# Stable Diffusion Web UI
|
# 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
|
To build for production: `npm build` / `yarn build`
|
||||||
|
|
||||||
`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
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- Search repo for "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.
|
- 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
|
||||||
|
487
frontend/dist/assets/index.00a29a58.js
vendored
Normal file
487
frontend/dist/assets/index.00a29a58.js
vendored
Normal file
File diff suppressed because one or more lines are too long
488
frontend/dist/assets/index.528a8981.js
vendored
488
frontend/dist/assets/index.528a8981.js
vendored
File diff suppressed because one or more lines are too long
2
frontend/dist/index.html
vendored
2
frontend/dist/index.html
vendored
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Stable Diffusion Dream Server</title>
|
<title>Stable Diffusion Dream Server</title>
|
||||||
<script type="module" crossorigin src="/assets/index.528a8981.js"></script>
|
<script type="module" crossorigin src="/assets/index.00a29a58.js"></script>
|
||||||
<link rel="stylesheet" href="/assets/index.447eb2a9.css">
|
<link rel="stylesheet" href="/assets/index.447eb2a9.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -4,8 +4,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsc-watch --onSuccess 'yarn run vite build -m development'",
|
"dev": "vite dev",
|
||||||
"hmr": "vite dev",
|
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"build-dev": "tsc && vite build -m development",
|
"build-dev": "tsc && vite build -m development",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
|
@ -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;
|
|
68
frontend/src/app/App.tsx
Normal file
68
frontend/src/app/App.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
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/header/ProgressBar';
|
||||||
|
import SiteHeader from '../features/header/SiteHeader';
|
||||||
|
import OptionsAccordion from '../features/sd/OptionsAccordion';
|
||||||
|
import ProcessButtons from '../features/sd/ProcessButtons';
|
||||||
|
import PromptInput from '../features/sd/PromptInput';
|
||||||
|
import LogViewer from '../features/system/LogViewer';
|
||||||
|
import Loading from '../Loading';
|
||||||
|
import { useAppDispatch } from './store';
|
||||||
|
import { requestAllImages } from './socketio/actions';
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const [isReady, setIsReady] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// Load images from the gallery once
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(requestAllImages());
|
||||||
|
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
|
// Valid samplers
|
||||||
export const SAMPLERS: Array<string> = [
|
export const SAMPLERS: Array<string> = [
|
||||||
'ddim',
|
'ddim',
|
||||||
'plms',
|
'plms',
|
||||||
'k_lms',
|
'k_lms',
|
||||||
'k_dpm_2',
|
'k_dpm_2',
|
||||||
'k_dpm_2_a',
|
'k_dpm_2_a',
|
||||||
'k_euler',
|
'k_euler',
|
||||||
'k_euler_a',
|
'k_euler_a',
|
||||||
'k_heun',
|
'k_heun',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Valid image widths
|
// Valid image widths
|
||||||
export const WIDTHS: Array<number> = [
|
export const WIDTHS: Array<number> = [
|
||||||
64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960,
|
64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960,
|
||||||
1024,
|
1024,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Valid image heights
|
// Valid image heights
|
||||||
export const HEIGHTS: Array<number> = [
|
export const HEIGHTS: Array<number> = [
|
||||||
64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960,
|
64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960,
|
||||||
1024,
|
1024,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Valid upscaling levels
|
// Valid upscaling levels
|
||||||
export const UPSCALING_LEVELS: Array<{ key: string; value: number }> = [
|
export const UPSCALING_LEVELS: Array<{ key: string; value: number }> = [
|
||||||
{ key: '2x', value: 2 },
|
{ key: '2x', value: 2 },
|
||||||
{ key: '4x', value: 4 },
|
{ key: '4x', value: 4 },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Internal to human-readable parameters
|
// Internal to human-readable parameters
|
||||||
export const PARAMETERS: { [key: string]: string } = {
|
export const PARAMETERS: { [key: string]: string } = {
|
||||||
prompt: 'Prompt',
|
prompt: 'Prompt',
|
||||||
iterations: 'Iterations',
|
iterations: 'Iterations',
|
||||||
steps: 'Steps',
|
steps: 'Steps',
|
||||||
cfgScale: 'CFG Scale',
|
cfgScale: 'CFG Scale',
|
||||||
height: 'Height',
|
height: 'Height',
|
||||||
width: 'Width',
|
width: 'Width',
|
||||||
sampler: 'Sampler',
|
sampler: 'Sampler',
|
||||||
seed: 'Seed',
|
seed: 'Seed',
|
||||||
img2imgStrength: 'img2img Strength',
|
img2imgStrength: 'img2img Strength',
|
||||||
gfpganStrength: 'GFPGAN Strength',
|
gfpganStrength: 'GFPGAN Strength',
|
||||||
upscalingLevel: 'Upscaling Level',
|
upscalingLevel: 'Upscaling Level',
|
||||||
upscalingStrength: 'Upscaling Strength',
|
upscalingStrength: 'Upscaling Strength',
|
||||||
initialImagePath: 'Initial Image',
|
initialImagePath: 'Initial Image',
|
||||||
maskPath: 'Initial Image Mask',
|
maskPath: 'Initial Image Mask',
|
||||||
shouldFitToWidthHeight: 'Fit Initial Image',
|
shouldFitToWidthHeight: 'Fit Initial Image',
|
||||||
seamless: 'Seamless Tiling',
|
seamless: 'Seamless Tiling',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NUMPY_RAND_MIN = 0;
|
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;
|
|
@ -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');
|
|
24
frontend/src/app/socketio/actions.ts
Normal file
24
frontend/src/app/socketio/actions.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
import { SDImage } from '../../features/gallery/gallerySlice';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<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');
|
101
frontend/src/app/socketio/emitters.ts
Normal file
101
frontend/src/app/socketio/emitters.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { AnyAction, Dispatch, MiddlewareAPI } from '@reduxjs/toolkit';
|
||||||
|
import dateFormat from 'dateformat';
|
||||||
|
import { Socket } from 'socket.io-client';
|
||||||
|
import { frontendToBackendParameters } from '../../common/util/parameterTranslation';
|
||||||
|
import { SDImage } from '../../features/gallery/gallerySlice';
|
||||||
|
import {
|
||||||
|
addLogEntry,
|
||||||
|
setIsProcessing,
|
||||||
|
} from '../../features/system/systemSlice';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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().sd, 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: SDImage) => {
|
||||||
|
dispatch(setIsProcessing(true));
|
||||||
|
const { upscalingLevel, upscalingStrength } = getState().sd;
|
||||||
|
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: SDImage) => {
|
||||||
|
dispatch(setIsProcessing(true));
|
||||||
|
const { gfpganStrength } = getState().sd;
|
||||||
|
|
||||||
|
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: SDImage) => {
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default makeSocketIOEmitters;
|
338
frontend/src/app/socketio/listeners.ts
Normal file
338
frontend/src/app/socketio/listeners.ts
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
import { AnyAction, MiddlewareAPI, Dispatch } from '@reduxjs/toolkit';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import dateFormat from 'dateformat';
|
||||||
|
|
||||||
|
import {
|
||||||
|
addLogEntry,
|
||||||
|
setIsConnected,
|
||||||
|
setIsProcessing,
|
||||||
|
SystemStatus,
|
||||||
|
setSystemStatus,
|
||||||
|
setCurrentStatus,
|
||||||
|
} from '../../features/system/systemSlice';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ServerGenerationResult,
|
||||||
|
ServerESRGANResult,
|
||||||
|
ServerGFPGANResult,
|
||||||
|
ServerIntermediateResult,
|
||||||
|
ServerError,
|
||||||
|
ServerGalleryImages,
|
||||||
|
ServerImageUrlAndUuid,
|
||||||
|
ServerImageUrl,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
import { backendToFrontendParameters } from '../../common/util/parameterTranslation';
|
||||||
|
|
||||||
|
import {
|
||||||
|
addImage,
|
||||||
|
clearIntermediateImage,
|
||||||
|
removeImage,
|
||||||
|
SDImage,
|
||||||
|
setGalleryImages,
|
||||||
|
setIntermediateImage,
|
||||||
|
} from '../../features/gallery/gallerySlice';
|
||||||
|
|
||||||
|
import { setInitialImagePath, setMaskPath } from '../../features/sd/sdSlice';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: ServerGenerationResult) => {
|
||||||
|
try {
|
||||||
|
const { url, metadata } = data;
|
||||||
|
const newUuid = uuidv4();
|
||||||
|
|
||||||
|
const translatedMetadata = backendToFrontendParameters(metadata);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
addImage({
|
||||||
|
uuid: newUuid,
|
||||||
|
url,
|
||||||
|
metadata: translatedMetadata,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
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: ServerIntermediateResult) => {
|
||||||
|
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: ServerESRGANResult) => {
|
||||||
|
try {
|
||||||
|
const { url, uuid, metadata } = data;
|
||||||
|
const newUuid = uuidv4();
|
||||||
|
|
||||||
|
// This image was only ESRGAN'd, grab the original image's metadata
|
||||||
|
const originalImage = getState().gallery.images.find(
|
||||||
|
(i: SDImage) => i.uuid === uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
// Retain the original metadata
|
||||||
|
const newMetadata = {
|
||||||
|
...originalImage.metadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the ESRGAN-related fields
|
||||||
|
newMetadata.shouldRunESRGAN = true;
|
||||||
|
newMetadata.upscalingLevel = metadata.upscale[0];
|
||||||
|
newMetadata.upscalingStrength = metadata.upscale[1];
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
addImage({
|
||||||
|
uuid: newUuid,
|
||||||
|
url,
|
||||||
|
metadata: newMetadata,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
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: ServerGFPGANResult) => {
|
||||||
|
try {
|
||||||
|
const { url, uuid, metadata } = data;
|
||||||
|
const newUuid = uuidv4();
|
||||||
|
|
||||||
|
// This image was only GFPGAN'd, grab the original image's metadata
|
||||||
|
const originalImage = getState().gallery.images.find(
|
||||||
|
(i: SDImage) => i.uuid === uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
// Retain the original metadata
|
||||||
|
const newMetadata = {
|
||||||
|
...originalImage.metadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the GFPGAN-related fields
|
||||||
|
newMetadata.shouldRunGFPGAN = true;
|
||||||
|
newMetadata.gfpganStrength = metadata.gfpgan_strength;
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
addImage({
|
||||||
|
uuid: newUuid,
|
||||||
|
url,
|
||||||
|
metadata: newMetadata,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
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: SystemStatus) => {
|
||||||
|
try {
|
||||||
|
dispatch(setIsProcessing(true));
|
||||||
|
dispatch(setSystemStatus(data));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Callback to run when we receive a 'progressUpdate' event.
|
||||||
|
*/
|
||||||
|
onError: (data: ServerError) => {
|
||||||
|
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: ServerGalleryImages) => {
|
||||||
|
const { images } = data;
|
||||||
|
const preparedImages = images.map((image): SDImage => {
|
||||||
|
return {
|
||||||
|
uuid: uuidv4(),
|
||||||
|
url: image.path,
|
||||||
|
metadata: backendToFrontendParameters(image.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: ServerImageUrlAndUuid) => {
|
||||||
|
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: ServerImageUrl) => {
|
||||||
|
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: ServerImageUrl) => {
|
||||||
|
const { url } = data;
|
||||||
|
dispatch(setMaskPath(url));
|
||||||
|
dispatch(
|
||||||
|
addLogEntry({
|
||||||
|
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||||
|
message: `Mask image uploaded: ${url}`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default makeSocketIOListeners;
|
157
frontend/src/app/socketio/middleware.ts
Normal file
157
frontend/src/app/socketio/middleware.ts
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import { Middleware } from '@reduxjs/toolkit';
|
||||||
|
import { io } from 'socket.io-client';
|
||||||
|
|
||||||
|
import makeSocketIOListeners from './listeners';
|
||||||
|
import makeSocketIOEmitters from './emitters';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ServerGenerationResult,
|
||||||
|
ServerESRGANResult,
|
||||||
|
ServerGFPGANResult,
|
||||||
|
ServerIntermediateResult,
|
||||||
|
ServerError,
|
||||||
|
ServerGalleryImages,
|
||||||
|
ServerImageUrlAndUuid,
|
||||||
|
ServerImageUrl,
|
||||||
|
} from './types';
|
||||||
|
import { SystemStatus } from '../../features/system/systemSlice';
|
||||||
|
|
||||||
|
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,
|
||||||
|
} = makeSocketIOListeners(store);
|
||||||
|
|
||||||
|
const {
|
||||||
|
emitGenerateImage,
|
||||||
|
emitRunESRGAN,
|
||||||
|
emitRunGFPGAN,
|
||||||
|
emitDeleteImage,
|
||||||
|
emitRequestAllImages,
|
||||||
|
emitCancelProcessing,
|
||||||
|
emitUploadInitialImage,
|
||||||
|
emitUploadMaskImage,
|
||||||
|
} = 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: ServerError) => onError(data));
|
||||||
|
|
||||||
|
socketio.on('generationResult', (data: ServerGenerationResult) =>
|
||||||
|
onGenerationResult(data)
|
||||||
|
);
|
||||||
|
|
||||||
|
socketio.on('esrganResult', (data: ServerESRGANResult) =>
|
||||||
|
onESRGANResult(data)
|
||||||
|
);
|
||||||
|
|
||||||
|
socketio.on('gfpganResult', (data: ServerGFPGANResult) =>
|
||||||
|
onGFPGANResult(data)
|
||||||
|
);
|
||||||
|
|
||||||
|
socketio.on('intermediateResult', (data: ServerIntermediateResult) =>
|
||||||
|
onIntermediateResult(data)
|
||||||
|
);
|
||||||
|
|
||||||
|
socketio.on('progressUpdate', (data: SystemStatus) =>
|
||||||
|
onProgressUpdate(data)
|
||||||
|
);
|
||||||
|
|
||||||
|
socketio.on('galleryImages', (data: ServerGalleryImages) =>
|
||||||
|
onGalleryImages(data)
|
||||||
|
);
|
||||||
|
|
||||||
|
socketio.on('processingCanceled', () => {
|
||||||
|
onProcessingCanceled();
|
||||||
|
});
|
||||||
|
|
||||||
|
socketio.on('imageDeleted', (data: ServerImageUrlAndUuid) => {
|
||||||
|
onImageDeleted(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socketio.on('initialImageUploaded', (data: ServerImageUrl) => {
|
||||||
|
onInitialImageUploaded(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socketio.on('maskImageUploaded', (data: ServerImageUrl) => {
|
||||||
|
onMaskImageUploaded(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next(action);
|
||||||
|
};
|
||||||
|
|
||||||
|
return middleware;
|
||||||
|
};
|
46
frontend/src/app/socketio/types.d.ts
vendored
Normal file
46
frontend/src/app/socketio/types.d.ts
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Interfaces used by the socketio middleware.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export declare interface ServerGenerationResult {
|
||||||
|
url: string;
|
||||||
|
metadata: { [key: string]: any };
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare interface ServerESRGANResult {
|
||||||
|
url: string;
|
||||||
|
uuid: string;
|
||||||
|
metadata: { [key: string]: any };
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare interface ServerGFPGANResult {
|
||||||
|
url: string;
|
||||||
|
uuid: string;
|
||||||
|
metadata: { [key: string]: any };
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare interface ServerIntermediateResult {
|
||||||
|
url: string;
|
||||||
|
metadata: { [key: string]: any };
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare interface ServerError {
|
||||||
|
message: string;
|
||||||
|
additionalData?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare interface ServerGalleryImages {
|
||||||
|
images: Array<{
|
||||||
|
path: string;
|
||||||
|
metadata: { [key: string]: any };
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare interface ServerImageUrlAndUuid {
|
||||||
|
uuid: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare interface ServerImageUrl {
|
||||||
|
url: string;
|
||||||
|
}
|
@ -1,53 +1,78 @@
|
|||||||
import { combineReducers, configureStore } from '@reduxjs/toolkit';
|
import { combineReducers, configureStore } from '@reduxjs/toolkit';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import type { TypedUseSelectorHook } from 'react-redux';
|
||||||
|
|
||||||
import { persistReducer } from 'redux-persist';
|
import { persistReducer } from 'redux-persist';
|
||||||
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web
|
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web
|
||||||
|
|
||||||
import sdReducer from '../features/sd/sdSlice';
|
import sdReducer from '../features/sd/sdSlice';
|
||||||
import galleryReducer from '../features/gallery/gallerySlice';
|
import galleryReducer from '../features/gallery/gallerySlice';
|
||||||
import systemReducer from '../features/system/systemSlice';
|
import systemReducer from '../features/system/systemSlice';
|
||||||
import { socketioMiddleware } from './socketio';
|
import { socketioMiddleware } from './socketio/middleware';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 rootPersistConfig = {
|
||||||
|
key: 'root',
|
||||||
|
storage,
|
||||||
|
blacklist: ['gallery', 'system'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const systemPersistConfig = {
|
||||||
|
key: 'system',
|
||||||
|
storage,
|
||||||
|
blacklist: [
|
||||||
|
'isConnected',
|
||||||
|
'isProcessing',
|
||||||
|
'currentStep',
|
||||||
|
'socketId',
|
||||||
|
'isESRGANAvailable',
|
||||||
|
'isGFPGANAvailable',
|
||||||
|
'currentStep',
|
||||||
|
'totalSteps',
|
||||||
|
'currentIteration',
|
||||||
|
'totalIterations',
|
||||||
|
'currentStatus',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
const reducers = combineReducers({
|
const reducers = combineReducers({
|
||||||
sd: sdReducer,
|
sd: sdReducer,
|
||||||
gallery: galleryReducer,
|
gallery: galleryReducer,
|
||||||
system: systemReducer,
|
system: persistReducer(systemPersistConfig, systemReducer),
|
||||||
});
|
});
|
||||||
|
|
||||||
const persistConfig = {
|
const persistedReducer = persistReducer(rootPersistConfig, reducers);
|
||||||
key: 'root',
|
|
||||||
storage,
|
|
||||||
};
|
|
||||||
|
|
||||||
const persistedReducer = persistReducer(persistConfig, reducers);
|
|
||||||
|
|
||||||
/*
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
// Continue with store setup
|
// Continue with store setup
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: persistedReducer,
|
reducer: persistedReducer,
|
||||||
middleware: (getDefaultMiddleware) =>
|
middleware: (getDefaultMiddleware) =>
|
||||||
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,
|
serializableCheck: false,
|
||||||
}).concat(socketioMiddleware()),
|
}).concat(socketioMiddleware()),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Infer the `RootState` and `AppDispatch` types from the store itself
|
|
||||||
export type RootState = ReturnType<typeof store.getState>;
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
|
|
||||||
export type AppDispatch = typeof store.dispatch;
|
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;
|
||||||
|
171
frontend/src/app/wip_types.ts
Normal file
171
frontend/src/app/wip_types.ts
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Defines common parameters required to generate an image.
|
||||||
|
* See #266 for the eventual maturation of this interface.
|
||||||
|
*/
|
||||||
|
interface CommonParameters {
|
||||||
|
/**
|
||||||
|
* The "txt2img" prompt. String. Minimum one character. No maximum.
|
||||||
|
*/
|
||||||
|
prompt: string;
|
||||||
|
/**
|
||||||
|
* The number of sampler steps. Integer. Minimum value 1. No maximum.
|
||||||
|
*/
|
||||||
|
steps: number;
|
||||||
|
/**
|
||||||
|
* Classifier-free guidance scale. Float. Minimum value 0. Maximum?
|
||||||
|
*/
|
||||||
|
cfgScale: number;
|
||||||
|
/**
|
||||||
|
* Height of output image in pixels. Integer. Minimum 64. Must be multiple of 64. No maximum.
|
||||||
|
*/
|
||||||
|
height: number;
|
||||||
|
/**
|
||||||
|
* Width of output image in pixels. Integer. Minimum 64. Must be multiple of 64. No maximum.
|
||||||
|
*/
|
||||||
|
width: number;
|
||||||
|
/**
|
||||||
|
* Name of the sampler to use. String. Restricted values.
|
||||||
|
*/
|
||||||
|
sampler:
|
||||||
|
| 'ddim'
|
||||||
|
| 'plms'
|
||||||
|
| 'k_lms'
|
||||||
|
| 'k_dpm_2'
|
||||||
|
| 'k_dpm_2_a'
|
||||||
|
| 'k_euler'
|
||||||
|
| 'k_euler_a'
|
||||||
|
| 'k_heun';
|
||||||
|
/**
|
||||||
|
* Seed used for randomness. Integer. 0 --> 4294967295, inclusive.
|
||||||
|
*/
|
||||||
|
seed: number;
|
||||||
|
/**
|
||||||
|
* Flag to enable seamless tiling image generation. Boolean.
|
||||||
|
*/
|
||||||
|
seamless: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines parameters needed to use the "img2img" generation method.
|
||||||
|
*/
|
||||||
|
interface ImageToImageParameters {
|
||||||
|
/**
|
||||||
|
* Folder path to the image used as the initial image. String.
|
||||||
|
*/
|
||||||
|
initialImagePath: string;
|
||||||
|
/**
|
||||||
|
* Flag to enable the use of a mask image during "img2img" generations.
|
||||||
|
* Requires valid ImageToImageParameters. Boolean.
|
||||||
|
*/
|
||||||
|
shouldUseMaskImage: boolean;
|
||||||
|
/**
|
||||||
|
* Folder path to the image used as a mask image. String.
|
||||||
|
*/
|
||||||
|
maskImagePath: string;
|
||||||
|
/**
|
||||||
|
* Strength of adherance to initial image. Float. 0 --> 1, exclusive.
|
||||||
|
*/
|
||||||
|
img2imgStrength: number;
|
||||||
|
/**
|
||||||
|
* Flag to enable the stretching of init image to desired output. Boolean.
|
||||||
|
*/
|
||||||
|
shouldFit: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the parameters needed to generate variations.
|
||||||
|
*/
|
||||||
|
interface VariationParameters {
|
||||||
|
/**
|
||||||
|
* Variation amount. Float. 0 --> 1, exclusive.
|
||||||
|
* TODO: What does this really do?
|
||||||
|
*/
|
||||||
|
variationAmount: number;
|
||||||
|
/**
|
||||||
|
* List of seed-weight pairs formatted as "seed:weight,...".
|
||||||
|
* Seed is a valid seed. Weight is a float, 0 --> 1, exclusive.
|
||||||
|
* String, must be parseable into [[seed,weight],...] format.
|
||||||
|
*/
|
||||||
|
seedWeights: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the parameters needed to use GFPGAN postprocessing.
|
||||||
|
*/
|
||||||
|
interface GFPGANParameters {
|
||||||
|
/**
|
||||||
|
* GFPGAN strength. Strength to apply face-fixing processing. Float. 0 --> 1, exclusive.
|
||||||
|
*/
|
||||||
|
gfpganStrength: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the parameters needed to use ESRGAN postprocessing.
|
||||||
|
*/
|
||||||
|
interface ESRGANParameters {
|
||||||
|
/**
|
||||||
|
* ESRGAN strength. Strength to apply upscaling. Float. 0 --> 1, exclusive.
|
||||||
|
*/
|
||||||
|
esrganStrength: number;
|
||||||
|
/**
|
||||||
|
* ESRGAN upscaling scale. One of 2x | 4x. Represented as integer.
|
||||||
|
*/
|
||||||
|
esrganScale: 2 | 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends the generation and processing method parameters, adding flags to enable each.
|
||||||
|
*/
|
||||||
|
interface ProcessingParameters extends CommonParameters {
|
||||||
|
/**
|
||||||
|
* Flag to enable the generation of variations. Requires valid VariationParameters. Boolean.
|
||||||
|
*/
|
||||||
|
shouldGenerateVariations: boolean;
|
||||||
|
/**
|
||||||
|
* Variation parameters.
|
||||||
|
*/
|
||||||
|
variationParameters: VariationParameters;
|
||||||
|
/**
|
||||||
|
* Flag to enable the use of an initial image, i.e. to use "img2img" generation.
|
||||||
|
* Requires valid ImageToImageParameters. Boolean.
|
||||||
|
*/
|
||||||
|
shouldUseImageToImage: boolean;
|
||||||
|
/**
|
||||||
|
* ImageToImage parameters.
|
||||||
|
*/
|
||||||
|
imageToImageParameters: ImageToImageParameters;
|
||||||
|
/**
|
||||||
|
* Flag to enable GFPGAN postprocessing. Requires valid GFPGANParameters. Boolean.
|
||||||
|
*/
|
||||||
|
shouldRunGFPGAN: boolean;
|
||||||
|
/**
|
||||||
|
* GFPGAN parameters.
|
||||||
|
*/
|
||||||
|
gfpganParameters: GFPGANParameters;
|
||||||
|
/**
|
||||||
|
* Flag to enable ESRGAN postprocessing. Requires valid ESRGANParameters. Boolean.
|
||||||
|
*/
|
||||||
|
shouldRunESRGAN: boolean;
|
||||||
|
/**
|
||||||
|
* ESRGAN parameters.
|
||||||
|
*/
|
||||||
|
esrganParameters: GFPGANParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends ProcessingParameters, adding items needed to request processing.
|
||||||
|
*/
|
||||||
|
interface ProcessingState extends ProcessingParameters {
|
||||||
|
/**
|
||||||
|
* Number of images to generate. Integer. Minimum 1.
|
||||||
|
*/
|
||||||
|
iterations: number;
|
||||||
|
/**
|
||||||
|
* Flag to enable the randomization of the seed on each generation. Boolean.
|
||||||
|
*/
|
||||||
|
shouldRandomizeSeed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export {}
|
21
frontend/src/common/components/SDButton.tsx
Normal file
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;
|
width?: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customized Chakra FormControl + NumberInput multi-part component.
|
||||||
|
*/
|
||||||
const SDNumberInput = (props: Props) => {
|
const SDNumberInput = (props: Props) => {
|
||||||
const {
|
const {
|
||||||
label,
|
label,
|
||||||
@ -31,7 +34,7 @@ const SDNumberInput = (props: Props) => {
|
|||||||
<Flex gap={2} justifyContent={'space-between'} alignItems={'center'}>
|
<Flex gap={2} justifyContent={'space-between'} alignItems={'center'}>
|
||||||
{label && (
|
{label && (
|
||||||
<FormLabel marginBottom={1}>
|
<FormLabel marginBottom={1}>
|
||||||
<Text fontSize={fontSize} whiteSpace='nowrap'>
|
<Text fontSize={fontSize} whiteSpace="nowrap">
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
@ -42,7 +45,7 @@ const SDNumberInput = (props: Props) => {
|
|||||||
keepWithinRange={false}
|
keepWithinRange={false}
|
||||||
clampValueOnBlur={true}
|
clampValueOnBlur={true}
|
||||||
>
|
>
|
||||||
<NumberInputField fontSize={'md'}/>
|
<NumberInputField fontSize={'md'} />
|
||||||
<NumberInputStepper>
|
<NumberInputStepper>
|
||||||
<NumberIncrementStepper />
|
<NumberIncrementStepper />
|
||||||
<NumberDecrementStepper />
|
<NumberDecrementStepper />
|
56
frontend/src/common/components/SDSelect.tsx
Normal file
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;
|
width?: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customized Chakra FormControl + Switch multi-part component.
|
||||||
|
*/
|
||||||
const SDSwitch = (props: Props) => {
|
const SDSwitch = (props: Props) => {
|
||||||
const {
|
const {
|
||||||
label,
|
label,
|
||||||
@ -28,7 +31,7 @@ const SDSwitch = (props: Props) => {
|
|||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
marginBottom={1}
|
marginBottom={1}
|
||||||
flexGrow={2}
|
flexGrow={2}
|
||||||
whiteSpace='nowrap'
|
whiteSpace="nowrap"
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</FormLabel>
|
</FormLabel>
|
104
frontend/src/common/hooks/useCheckParameters.ts
Normal file
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 { SDState } from '../../features/sd/sdSlice';
|
||||||
|
import { SystemState } from '../../features/system/systemSlice';
|
||||||
|
import { validateSeedWeights } from '../util/seedWeightPairs';
|
||||||
|
|
||||||
|
const sdSelector = createSelector(
|
||||||
|
(state: RootState) => state.sd,
|
||||||
|
(sd: SDState) => {
|
||||||
|
return {
|
||||||
|
prompt: sd.prompt,
|
||||||
|
shouldGenerateVariations: sd.shouldGenerateVariations,
|
||||||
|
seedWeights: sd.seedWeights,
|
||||||
|
maskPath: sd.maskPath,
|
||||||
|
initialImagePath: sd.initialImagePath,
|
||||||
|
seed: sd.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(sdSelector);
|
||||||
|
|
||||||
|
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;
|
@ -1,17 +1,15 @@
|
|||||||
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
|
These functions translate frontend state into parameters
|
||||||
suitable for consumption by the backend, and vice-versa.
|
suitable for consumption by the backend, and vice-versa.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from "../../app/constants";
|
||||||
|
import { SDState } from "../../features/sd/sdSlice";
|
||||||
|
import { SystemState } from "../../features/system/systemSlice";
|
||||||
|
import randomInt from "./randomInt";
|
||||||
|
import { seedWeightsToString, stringToSeedWeights } from "./seedWeightPairs";
|
||||||
|
|
||||||
export const frontendToBackendParameters = (
|
export const frontendToBackendParameters = (
|
||||||
sdState: SDState,
|
sdState: SDState,
|
||||||
systemState: SystemState
|
systemState: SystemState
|
||||||
@ -34,7 +32,7 @@ export const frontendToBackendParameters = (
|
|||||||
maskPath,
|
maskPath,
|
||||||
shouldFitToWidthHeight,
|
shouldFitToWidthHeight,
|
||||||
shouldGenerateVariations,
|
shouldGenerateVariations,
|
||||||
variantAmount,
|
variationAmount,
|
||||||
seedWeights,
|
seedWeights,
|
||||||
shouldRunESRGAN,
|
shouldRunESRGAN,
|
||||||
upscalingLevel,
|
upscalingLevel,
|
||||||
@ -75,7 +73,7 @@ export const frontendToBackendParameters = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (shouldGenerateVariations) {
|
if (shouldGenerateVariations) {
|
||||||
generationParameters.variation_amount = variantAmount;
|
generationParameters.variation_amount = variationAmount;
|
||||||
if (seedWeights) {
|
if (seedWeights) {
|
||||||
generationParameters.with_variations =
|
generationParameters.with_variations =
|
||||||
stringToSeedWeights(seedWeights);
|
stringToSeedWeights(seedWeights);
|
||||||
@ -144,7 +142,7 @@ export const backendToFrontendParameters = (parameters: {
|
|||||||
|
|
||||||
if (variation_amount > 0) {
|
if (variation_amount > 0) {
|
||||||
sd.shouldGenerateVariations = true;
|
sd.shouldGenerateVariations = true;
|
||||||
sd.variantAmount = variation_amount;
|
sd.variationAmount = variation_amount;
|
||||||
if (with_variations) {
|
if (with_variations) {
|
||||||
sd.seedWeights = seedWeightsToString(with_variations);
|
sd.seedWeights = seedWeightsToString(with_variations);
|
||||||
}
|
}
|
@ -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;
|
|
149
frontend/src/features/gallery/CurrentImageButtons.tsx
Normal file
149
frontend/src/features/gallery/CurrentImageButtons.tsx
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import { Flex } from '@chakra-ui/react';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
|
import { RootState } from '../../app/store';
|
||||||
|
import { setAllParameters, setInitialImagePath, setSeed } from '../sd/sdSlice';
|
||||||
|
import DeleteImageModal from './DeleteImageModal';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { SystemState } from '../system/systemSlice';
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
|
import { SDImage } from './gallerySlice';
|
||||||
|
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: SDImage;
|
||||||
|
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.sd
|
||||||
|
);
|
||||||
|
|
||||||
|
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.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'}
|
||||||
|
onClick={handleClickUseAllParameters}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SDButton
|
||||||
|
label="Use seed"
|
||||||
|
colorScheme={'gray'}
|
||||||
|
flexGrow={1}
|
||||||
|
variant={'outline'}
|
||||||
|
isDisabled={!image.metadata.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
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;
|
121
frontend/src/features/gallery/DeleteImageModal.tsx
Normal file
121
frontend/src/features/gallery/DeleteImageModal.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
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,
|
||||||
|
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 { SDImage } from './gallerySlice';
|
||||||
|
|
||||||
|
interface DeleteImageModalProps {
|
||||||
|
/**
|
||||||
|
* Component which, on click, should delete the image/open the modal.
|
||||||
|
*/
|
||||||
|
children: ReactElement;
|
||||||
|
/**
|
||||||
|
* The image to delete.
|
||||||
|
*/
|
||||||
|
image: SDImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = ({ image, children }: DeleteImageModalProps) => {
|
||||||
|
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,
|
||||||
|
})}
|
||||||
|
|
||||||
|
<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;
|
|
131
frontend/src/features/gallery/HoverableImage.tsx
Normal file
131
frontend/src/features/gallery/HoverableImage.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
Image,
|
||||||
|
useColorModeValue,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useAppDispatch } from '../../app/store';
|
||||||
|
import { SDImage, setCurrentImage } from './gallerySlice';
|
||||||
|
import { FaCheck, FaCopy, FaSeedling, FaTrash } from 'react-icons/fa';
|
||||||
|
import DeleteImageModal from './DeleteImageModal';
|
||||||
|
import { memo, SyntheticEvent, useState } from 'react';
|
||||||
|
import { setAllParameters, setSeed } from '../sd/sdSlice';
|
||||||
|
|
||||||
|
interface HoverableImageProps {
|
||||||
|
image: SDImage;
|
||||||
|
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.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}
|
||||||
|
>
|
||||||
|
<DeleteImageModal image={image}>
|
||||||
|
<IconButton
|
||||||
|
colorScheme="red"
|
||||||
|
aria-label="Delete image"
|
||||||
|
icon={<FaTrash />}
|
||||||
|
size="xs"
|
||||||
|
fontSize={15}
|
||||||
|
/>
|
||||||
|
</DeleteImageModal>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}, memoEqualityCheck);
|
||||||
|
|
||||||
|
export default HoverableImage;
|
39
frontend/src/features/gallery/ImageGallery.tsx
Normal file
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,134 @@
|
|||||||
import {
|
import {
|
||||||
Center,
|
Center,
|
||||||
Flex,
|
Flex,
|
||||||
IconButton,
|
IconButton,
|
||||||
Link,
|
Link,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
Text,
|
Text,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
|
import { memo } from 'react';
|
||||||
import { FaPlus } from 'react-icons/fa';
|
import { FaPlus } from 'react-icons/fa';
|
||||||
import { PARAMETERS } from '../../app/constants';
|
import { PARAMETERS } from '../../app/constants';
|
||||||
import { useAppDispatch } from '../../app/hooks';
|
import { useAppDispatch } from '../../app/store';
|
||||||
import SDButton from '../../components/SDButton';
|
import SDButton from '../../common/components/SDButton';
|
||||||
import { setAllParameters, setParameter } from '../sd/sdSlice';
|
import { setAllParameters, setParameter } from '../sd/sdSlice';
|
||||||
import { SDImage, SDMetadata } from './gallerySlice';
|
import { SDImage, SDMetadata } from './gallerySlice';
|
||||||
|
|
||||||
type Props = {
|
type ImageMetadataViewerProps = {
|
||||||
image: SDImage;
|
image: SDImage;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ImageMetadataViewer = ({ image }: Props) => {
|
// TODO: I don't know if this is needed.
|
||||||
const dispatch = useAppDispatch();
|
const memoEqualityCheck = (
|
||||||
|
prev: ImageMetadataViewerProps,
|
||||||
|
next: ImageMetadataViewerProps
|
||||||
|
) => prev.image.uuid === next.image.uuid;
|
||||||
|
|
||||||
const keys = Object.keys(PARAMETERS);
|
// TODO: Show more interesting information in this component.
|
||||||
|
|
||||||
const metadata: Array<{
|
/**
|
||||||
label: string;
|
* Image metadata viewer overlays currently selected image and provides
|
||||||
key: string;
|
* access to any of its metadata for use in processing.
|
||||||
value: string | number | boolean;
|
*/
|
||||||
}> = [];
|
const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
keys.forEach((key) => {
|
/**
|
||||||
const value = image.metadata[key as keyof SDMetadata];
|
* Build an array representing each item of metadata and a human-readable
|
||||||
if (value !== undefined) {
|
* label for it e.g. "cfgScale" > "CFG Scale".
|
||||||
metadata.push({ label: PARAMETERS[key], key, value });
|
*
|
||||||
}
|
* This array is then used to render each item with a button to use that
|
||||||
});
|
* parameter in the processing settings.
|
||||||
|
*
|
||||||
|
* TODO: All this logic feels sloppy.
|
||||||
|
*/
|
||||||
|
const keys = Object.keys(PARAMETERS);
|
||||||
|
|
||||||
return (
|
const metadata: Array<{
|
||||||
<Flex gap={2} direction={'column'} overflowY={'scroll'} width={'100%'}>
|
label: string;
|
||||||
<SDButton
|
key: string;
|
||||||
label='Use all parameters'
|
value: string | number | boolean;
|
||||||
colorScheme={'gray'}
|
}> = [];
|
||||||
padding={2}
|
|
||||||
isDisabled={metadata.length === 0}
|
|
||||||
onClick={() => dispatch(setAllParameters(image.metadata))}
|
|
||||||
/>
|
|
||||||
<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 ||
|
keys.forEach((key) => {
|
||||||
value === null ||
|
const value = image.metadata[key as keyof SDMetadata];
|
||||||
value === '' ||
|
if (value !== undefined) {
|
||||||
value === 0 ? (
|
metadata.push({ label: PARAMETERS[key], key, value });
|
||||||
<Text
|
}
|
||||||
maxHeight={100}
|
});
|
||||||
fontStyle={'italic'}
|
|
||||||
>
|
return (
|
||||||
None
|
<Flex gap={2} direction={'column'} overflowY={'scroll'} width={'100%'}>
|
||||||
</Text>
|
<SDButton
|
||||||
) : (
|
label="Use all parameters"
|
||||||
<Text
|
colorScheme={'gray'}
|
||||||
maxHeight={100}
|
padding={2}
|
||||||
overflowY={'scroll'}
|
isDisabled={metadata.length === 0}
|
||||||
>
|
onClick={() => dispatch(setAllParameters(image.metadata))}
|
||||||
{value.toString()}
|
/>
|
||||||
</Text>
|
<Flex gap={2}>
|
||||||
)}
|
<Text fontWeight={'semibold'}>File:</Text>
|
||||||
</Flex>
|
<Link href={image.url} isExternal>
|
||||||
</ListItem>
|
<Text>{image.url}</Text>
|
||||||
);
|
</Link>
|
||||||
})}
|
</Flex>
|
||||||
</List>
|
{metadata.length ? (
|
||||||
<Flex gap={2}>
|
<>
|
||||||
<Text fontWeight={'semibold'}>Raw:</Text>
|
<List>
|
||||||
<Text
|
{metadata.map((parameter, i) => {
|
||||||
maxHeight={100}
|
const { label, key, value } = parameter;
|
||||||
overflowY={'scroll'}
|
return (
|
||||||
wordBreak={'break-all'}
|
<ListItem key={i} pb={1}>
|
||||||
>
|
<Flex gap={2}>
|
||||||
{JSON.stringify(image.metadata)}
|
<IconButton
|
||||||
</Text>
|
aria-label="Use this parameter"
|
||||||
</Flex>
|
icon={<FaPlus />}
|
||||||
</>
|
size={'xs'}
|
||||||
) : (
|
onClick={() =>
|
||||||
<Center width={'100%'} pt={10}>
|
dispatch(
|
||||||
<Text fontSize={'lg'} fontWeight='semibold'>
|
setParameter({
|
||||||
No metadata available
|
key,
|
||||||
</Text>
|
value,
|
||||||
</Center>
|
})
|
||||||
)}
|
)
|
||||||
</Flex>
|
}
|
||||||
);
|
/>
|
||||||
};
|
<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>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}, memoEqualityCheck);
|
||||||
|
|
||||||
export default ImageMetadataViewer;
|
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,8 +1,7 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { UpscalingLevel } from '../sd/sdSlice';
|
import { UpscalingLevel } from '../sd/sdSlice';
|
||||||
import { backendToFrontendParameters } from '../../app/parameterTranslation';
|
import { clamp } from 'lodash';
|
||||||
|
|
||||||
// TODO: Revise pending metadata RFC: https://github.com/lstein/stable-diffusion/issues/266
|
// TODO: Revise pending metadata RFC: https://github.com/lstein/stable-diffusion/issues/266
|
||||||
export interface SDMetadata {
|
export interface SDMetadata {
|
||||||
@ -52,29 +51,48 @@ export const gallerySlice = createSlice({
|
|||||||
state.currentImage = action.payload;
|
state.currentImage = action.payload;
|
||||||
state.currentImageUuid = action.payload.uuid;
|
state.currentImageUuid = action.payload.uuid;
|
||||||
},
|
},
|
||||||
removeImage: (state, action: PayloadAction<SDImage>) => {
|
removeImage: (state, action: PayloadAction<string>) => {
|
||||||
const { uuid } = action.payload;
|
const uuid = action.payload;
|
||||||
|
|
||||||
const newImages = state.images.filter((image) => image.uuid !== uuid);
|
const newImages = state.images.filter((image) => image.uuid !== uuid);
|
||||||
|
|
||||||
const imageToDeleteIndex = state.images.findIndex(
|
if (uuid === state.currentImageUuid) {
|
||||||
(image) => image.uuid === uuid
|
/**
|
||||||
);
|
* 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),
|
* New current image needs to be in the same spot, but because the gallery
|
||||||
newImages.length - 1
|
* 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.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<SDImage>) => {
|
||||||
state.images.push(action.payload);
|
state.images.push(action.payload);
|
||||||
@ -88,47 +106,13 @@ export const gallerySlice = createSlice({
|
|||||||
clearIntermediateImage: (state) => {
|
clearIntermediateImage: (state) => {
|
||||||
state.intermediateImage = undefined;
|
state.intermediateImage = undefined;
|
||||||
},
|
},
|
||||||
setGalleryImages: (
|
setGalleryImages: (state, action: PayloadAction<Array<SDImage>>) => {
|
||||||
state,
|
const newImages = action.payload;
|
||||||
action: PayloadAction<
|
if (newImages.length) {
|
||||||
Array<{
|
const newCurrentImage = newImages[newImages.length - 1];
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.images = newImages;
|
state.images = newImages;
|
||||||
|
state.currentImage = newCurrentImage;
|
||||||
|
state.currentImageUuid = newCurrentImage.uuid;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,35 +1,38 @@
|
|||||||
import { Progress } from '@chakra-ui/react';
|
import { Progress } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { useAppSelector } from '../../app/hooks';
|
import { useAppSelector } from '../../app/store';
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from '../../app/store';
|
||||||
import { SDState } from '../sd/sdSlice';
|
import { SystemState } from '../system/systemSlice';
|
||||||
|
|
||||||
const sdSelector = createSelector(
|
const systemSelector = createSelector(
|
||||||
(state: RootState) => state.sd,
|
(state: RootState) => state.system,
|
||||||
(sd: SDState) => {
|
(system: SystemState) => {
|
||||||
return {
|
return {
|
||||||
realSteps: sd.realSteps,
|
isProcessing: system.isProcessing,
|
||||||
};
|
currentStep: system.currentStep,
|
||||||
},
|
totalSteps: system.totalSteps,
|
||||||
{
|
currentStatusHasSteps: system.currentStatusHasSteps,
|
||||||
memoizeOptions: {
|
};
|
||||||
resultEqualityCheck: isEqual,
|
},
|
||||||
},
|
{
|
||||||
}
|
memoizeOptions: { resultEqualityCheck: isEqual },
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const ProgressBar = () => {
|
const ProgressBar = () => {
|
||||||
const { realSteps } = useAppSelector(sdSelector);
|
const { isProcessing, currentStep, totalSteps, currentStatusHasSteps } =
|
||||||
const { currentStep } = useAppSelector((state: RootState) => state.system);
|
useAppSelector(systemSelector);
|
||||||
const progress = Math.round((currentStep * 100) / realSteps);
|
|
||||||
return (
|
const value = currentStep ? Math.round((currentStep * 100) / totalSteps) : 0;
|
||||||
<Progress
|
|
||||||
height='10px'
|
return (
|
||||||
value={progress}
|
<Progress
|
||||||
isIndeterminate={progress < 0 || currentStep === realSteps}
|
height="10px"
|
||||||
/>
|
value={value}
|
||||||
);
|
isIndeterminate={isProcessing && !currentStatusHasSteps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProgressBar;
|
export default ProgressBar;
|
||||||
|
@ -12,39 +12,66 @@ import { isEqual } from 'lodash';
|
|||||||
|
|
||||||
import { FaSun, FaMoon, FaGithub } from 'react-icons/fa';
|
import { FaSun, FaMoon, FaGithub } from 'react-icons/fa';
|
||||||
import { MdHelp, MdSettings } from 'react-icons/md';
|
import { MdHelp, MdSettings } from 'react-icons/md';
|
||||||
import { useAppSelector } from '../../app/hooks';
|
import { useAppSelector } from '../../app/store';
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from '../../app/store';
|
||||||
import SettingsModal from '../system/SettingsModal';
|
import SettingsModal from '../system/SettingsModal';
|
||||||
import { SystemState } from '../system/systemSlice';
|
import { SystemState } from '../system/systemSlice';
|
||||||
|
|
||||||
const systemSelector = createSelector(
|
const systemSelector = createSelector(
|
||||||
(state: RootState) => state.system,
|
(state: RootState) => state.system,
|
||||||
(system: SystemState) => {
|
(system: SystemState) => {
|
||||||
return { isConnected: system.isConnected };
|
return {
|
||||||
|
isConnected: system.isConnected,
|
||||||
|
isProcessing: system.isProcessing,
|
||||||
|
currentIteration: system.currentIteration,
|
||||||
|
totalIterations: system.totalIterations,
|
||||||
|
currentStatus: system.currentStatus,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
memoizeOptions: { resultEqualityCheck: isEqual },
|
memoizeOptions: { resultEqualityCheck: isEqual },
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Header, includes color mode toggle, settings button, status message.
|
||||||
|
*/
|
||||||
const SiteHeader = () => {
|
const SiteHeader = () => {
|
||||||
const { colorMode, toggleColorMode } = useColorMode();
|
const { colorMode, toggleColorMode } = useColorMode();
|
||||||
const { isConnected } = useAppSelector(systemSelector);
|
const {
|
||||||
|
isConnected,
|
||||||
|
isProcessing,
|
||||||
|
currentIteration,
|
||||||
|
totalIterations,
|
||||||
|
currentStatus,
|
||||||
|
} = useAppSelector(systemSelector);
|
||||||
|
|
||||||
|
const statusMessageTextColor = isConnected ? 'green.500' : 'red.500';
|
||||||
|
|
||||||
|
const colorModeIcon = colorMode == 'light' ? <FaMoon /> : <FaSun />;
|
||||||
|
|
||||||
|
// Make FaMoon and FaSun icon apparent size consistent
|
||||||
|
const colorModeIconFontSize = colorMode == 'light' ? 18 : 20;
|
||||||
|
|
||||||
|
let statusMessage = currentStatus;
|
||||||
|
|
||||||
|
if (isProcessing) {
|
||||||
|
if (totalIterations > 1) {
|
||||||
|
statusMessage += ` [${currentIteration}/${totalIterations}]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex minWidth='max-content' alignItems='center' gap='1' pl={2} pr={1}>
|
<Flex minWidth="max-content" alignItems="center" gap="1" pl={2} pr={1}>
|
||||||
<Heading size={'lg'}>Stable Diffusion Dream Server</Heading>
|
<Heading size={'lg'}>Stable Diffusion Dream Server</Heading>
|
||||||
|
|
||||||
<Spacer />
|
<Spacer />
|
||||||
|
|
||||||
<Text textColor={isConnected ? 'green.500' : 'red.500'}>
|
<Text textColor={statusMessageTextColor}>{statusMessage}</Text>
|
||||||
{isConnected ? `Connected to server` : 'No connection to server'}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<SettingsModal>
|
<SettingsModal>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label='Settings'
|
aria-label="Settings"
|
||||||
variant='link'
|
variant="link"
|
||||||
fontSize={24}
|
fontSize={24}
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
icon={<MdSettings />}
|
icon={<MdSettings />}
|
||||||
@ -52,14 +79,14 @@ const SiteHeader = () => {
|
|||||||
</SettingsModal>
|
</SettingsModal>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label='Link to Github Issues'
|
aria-label="Link to Github Issues"
|
||||||
variant='link'
|
variant="link"
|
||||||
fontSize={23}
|
fontSize={23}
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
icon={
|
icon={
|
||||||
<Link
|
<Link
|
||||||
isExternal
|
isExternal
|
||||||
href='http://github.com/lstein/stable-diffusion/issues'
|
href="http://github.com/lstein/stable-diffusion/issues"
|
||||||
>
|
>
|
||||||
<MdHelp />
|
<MdHelp />
|
||||||
</Link>
|
</Link>
|
||||||
@ -67,24 +94,24 @@ const SiteHeader = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label='Link to Github Repo'
|
aria-label="Link to Github Repo"
|
||||||
variant='link'
|
variant="link"
|
||||||
fontSize={20}
|
fontSize={20}
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
icon={
|
icon={
|
||||||
<Link isExternal href='http://github.com/lstein/stable-diffusion'>
|
<Link isExternal href="http://github.com/lstein/stable-diffusion">
|
||||||
<FaGithub />
|
<FaGithub />
|
||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label='Toggle Dark Mode'
|
aria-label="Toggle Dark Mode"
|
||||||
onClick={toggleColorMode}
|
onClick={toggleColorMode}
|
||||||
variant='link'
|
variant="link"
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
fontSize={colorMode == 'light' ? 18 : 20}
|
fontSize={colorModeIconFontSize}
|
||||||
icon={colorMode == 'light' ? <FaMoon /> : <FaSun />}
|
icon={colorModeIcon}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
@ -1,84 +1,87 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex } from '@chakra-ui/react';
|
||||||
|
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from '../../app/store';
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
setUpscalingLevel,
|
setUpscalingLevel,
|
||||||
setUpscalingStrength,
|
setUpscalingStrength,
|
||||||
UpscalingLevel,
|
UpscalingLevel,
|
||||||
SDState,
|
SDState,
|
||||||
} from '../sd/sdSlice';
|
} from '../sd/sdSlice';
|
||||||
|
|
||||||
import SDNumberInput from '../../components/SDNumberInput';
|
|
||||||
import SDSelect from '../../components/SDSelect';
|
|
||||||
|
|
||||||
import { UPSCALING_LEVELS } from '../../app/constants';
|
import { UPSCALING_LEVELS } from '../../app/constants';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { SystemState } from '../system/systemSlice';
|
import { SystemState } from '../system/systemSlice';
|
||||||
|
import { ChangeEvent } from 'react';
|
||||||
|
import SDNumberInput from '../../common/components/SDNumberInput';
|
||||||
|
import SDSelect from '../../common/components/SDSelect';
|
||||||
|
|
||||||
const sdSelector = createSelector(
|
const sdSelector = createSelector(
|
||||||
(state: RootState) => state.sd,
|
(state: RootState) => state.sd,
|
||||||
(sd: SDState) => {
|
(sd: SDState) => {
|
||||||
return {
|
return {
|
||||||
upscalingLevel: sd.upscalingLevel,
|
upscalingLevel: sd.upscalingLevel,
|
||||||
upscalingStrength: sd.upscalingStrength,
|
upscalingStrength: sd.upscalingStrength,
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: isEqual,
|
||||||
},
|
},
|
||||||
{
|
}
|
||||||
memoizeOptions: {
|
|
||||||
resultEqualityCheck: isEqual,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const systemSelector = createSelector(
|
const systemSelector = createSelector(
|
||||||
(state: RootState) => state.system,
|
(state: RootState) => state.system,
|
||||||
(system: SystemState) => {
|
(system: SystemState) => {
|
||||||
return {
|
return {
|
||||||
isESRGANAvailable: system.isESRGANAvailable,
|
isESRGANAvailable: system.isESRGANAvailable,
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: isEqual,
|
||||||
},
|
},
|
||||||
{
|
}
|
||||||
memoizeOptions: {
|
|
||||||
resultEqualityCheck: isEqual,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays upscaling/ESRGAN options (level and strength).
|
||||||
|
*/
|
||||||
const ESRGANOptions = () => {
|
const ESRGANOptions = () => {
|
||||||
const { upscalingLevel, upscalingStrength } = useAppSelector(sdSelector);
|
const dispatch = useAppDispatch();
|
||||||
|
const { upscalingLevel, upscalingStrength } = useAppSelector(sdSelector);
|
||||||
|
const { isESRGANAvailable } = useAppSelector(systemSelector);
|
||||||
|
|
||||||
const { isESRGANAvailable } = useAppSelector(systemSelector);
|
const handleChangeLevel = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||||
|
dispatch(setUpscalingLevel(Number(e.target.value) as UpscalingLevel));
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const handleChangeStrength = (v: string | number) =>
|
||||||
|
dispatch(setUpscalingStrength(Number(v)));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction={'column'} gap={2}>
|
<Flex direction={'column'} gap={2}>
|
||||||
<SDSelect
|
<SDSelect
|
||||||
isDisabled={!isESRGANAvailable}
|
isDisabled={!isESRGANAvailable}
|
||||||
label='Scale'
|
label="Scale"
|
||||||
value={upscalingLevel}
|
value={upscalingLevel}
|
||||||
onChange={(e) =>
|
onChange={handleChangeLevel}
|
||||||
dispatch(
|
validValues={UPSCALING_LEVELS}
|
||||||
setUpscalingLevel(
|
/>
|
||||||
Number(e.target.value) as UpscalingLevel
|
<SDNumberInput
|
||||||
)
|
isDisabled={!isESRGANAvailable}
|
||||||
)
|
label="Strength"
|
||||||
}
|
step={0.05}
|
||||||
validValues={UPSCALING_LEVELS}
|
min={0}
|
||||||
/>
|
max={1}
|
||||||
<SDNumberInput
|
onChange={handleChangeStrength}
|
||||||
isDisabled={!isESRGANAvailable}
|
value={upscalingStrength}
|
||||||
label='Strength'
|
/>
|
||||||
step={0.05}
|
</Flex>
|
||||||
min={0}
|
);
|
||||||
max={1}
|
|
||||||
onChange={(v) => dispatch(setUpscalingStrength(Number(v)))}
|
|
||||||
value={upscalingStrength}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ESRGANOptions;
|
export default ESRGANOptions;
|
||||||
|
@ -1,63 +1,68 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex } from '@chakra-ui/react';
|
||||||
|
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from '../../app/store';
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
|
|
||||||
import { SDState, setGfpganStrength } from '../sd/sdSlice';
|
import { SDState, setGfpganStrength } from '../sd/sdSlice';
|
||||||
|
|
||||||
import SDNumberInput from '../../components/SDNumberInput';
|
|
||||||
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { SystemState } from '../system/systemSlice';
|
import { SystemState } from '../system/systemSlice';
|
||||||
|
import SDNumberInput from '../../common/components/SDNumberInput';
|
||||||
|
|
||||||
const sdSelector = createSelector(
|
const sdSelector = createSelector(
|
||||||
(state: RootState) => state.sd,
|
(state: RootState) => state.sd,
|
||||||
(sd: SDState) => {
|
(sd: SDState) => {
|
||||||
return {
|
return {
|
||||||
gfpganStrength: sd.gfpganStrength,
|
gfpganStrength: sd.gfpganStrength,
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: isEqual,
|
||||||
},
|
},
|
||||||
{
|
}
|
||||||
memoizeOptions: {
|
|
||||||
resultEqualityCheck: isEqual,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const systemSelector = createSelector(
|
const systemSelector = createSelector(
|
||||||
(state: RootState) => state.system,
|
(state: RootState) => state.system,
|
||||||
(system: SystemState) => {
|
(system: SystemState) => {
|
||||||
return {
|
return {
|
||||||
isGFPGANAvailable: system.isGFPGANAvailable,
|
isGFPGANAvailable: system.isGFPGANAvailable,
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: isEqual,
|
||||||
},
|
},
|
||||||
{
|
}
|
||||||
memoizeOptions: {
|
|
||||||
resultEqualityCheck: isEqual,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays face-fixing/GFPGAN options (strength).
|
||||||
|
*/
|
||||||
const GFPGANOptions = () => {
|
const GFPGANOptions = () => {
|
||||||
const { gfpganStrength } = useAppSelector(sdSelector);
|
const dispatch = useAppDispatch();
|
||||||
|
const { gfpganStrength } = useAppSelector(sdSelector);
|
||||||
|
const { isGFPGANAvailable } = useAppSelector(systemSelector);
|
||||||
|
|
||||||
const { isGFPGANAvailable } = useAppSelector(systemSelector);
|
const handleChangeStrength = (v: string | number) =>
|
||||||
|
dispatch(setGfpganStrength(Number(v)));
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
return (
|
||||||
|
<Flex direction={'column'} gap={2}>
|
||||||
return (
|
<SDNumberInput
|
||||||
<Flex direction={'column'} gap={2}>
|
isDisabled={!isGFPGANAvailable}
|
||||||
<SDNumberInput
|
label="Strength"
|
||||||
isDisabled={!isGFPGANAvailable}
|
step={0.05}
|
||||||
label='Strength'
|
min={0}
|
||||||
step={0.05}
|
max={1}
|
||||||
min={0}
|
onChange={handleChangeStrength}
|
||||||
max={1}
|
value={gfpganStrength}
|
||||||
onChange={(v) => dispatch(setGfpganStrength(Number(v)))}
|
/>
|
||||||
value={gfpganStrength}
|
</Flex>
|
||||||
/>
|
);
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GFPGANOptions;
|
export default GFPGANOptions;
|
||||||
|
@ -1,54 +1,59 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
import { ChangeEvent } from 'react';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from '../../app/store';
|
||||||
import SDNumberInput from '../../components/SDNumberInput';
|
import SDNumberInput from '../../common/components/SDNumberInput';
|
||||||
import SDSwitch from '../../components/SDSwitch';
|
import SDSwitch from '../../common/components/SDSwitch';
|
||||||
import InitImage from './InitImage';
|
import InitAndMaskImage from './InitAndMaskImage';
|
||||||
import {
|
import {
|
||||||
SDState,
|
SDState,
|
||||||
setImg2imgStrength,
|
setImg2imgStrength,
|
||||||
setShouldFitToWidthHeight,
|
setShouldFitToWidthHeight,
|
||||||
} from './sdSlice';
|
} from './sdSlice';
|
||||||
|
|
||||||
const sdSelector = createSelector(
|
const sdSelector = createSelector(
|
||||||
(state: RootState) => state.sd,
|
(state: RootState) => state.sd,
|
||||||
(sd: SDState) => {
|
(sd: SDState) => {
|
||||||
return {
|
return {
|
||||||
initialImagePath: sd.initialImagePath,
|
img2imgStrength: sd.img2imgStrength,
|
||||||
img2imgStrength: sd.img2imgStrength,
|
shouldFitToWidthHeight: sd.shouldFitToWidthHeight,
|
||||||
shouldFitToWidthHeight: sd.shouldFitToWidthHeight,
|
};
|
||||||
};
|
}
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for img2img generation (strength, fit, init/mask upload).
|
||||||
|
*/
|
||||||
const ImageToImageOptions = () => {
|
const ImageToImageOptions = () => {
|
||||||
const { initialImagePath, img2imgStrength, shouldFitToWidthHeight } =
|
const dispatch = useAppDispatch();
|
||||||
useAppSelector(sdSelector);
|
const { img2imgStrength, shouldFitToWidthHeight } =
|
||||||
|
useAppSelector(sdSelector);
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const handleChangeStrength = (v: string | number) =>
|
||||||
return (
|
dispatch(setImg2imgStrength(Number(v)));
|
||||||
<Flex direction={'column'} gap={2}>
|
|
||||||
<SDNumberInput
|
const handleChangeFit = (e: ChangeEvent<HTMLInputElement>) =>
|
||||||
isDisabled={!initialImagePath}
|
dispatch(setShouldFitToWidthHeight(e.target.checked));
|
||||||
label='Strength'
|
|
||||||
step={0.01}
|
return (
|
||||||
min={0}
|
<Flex direction={'column'} gap={2}>
|
||||||
max={1}
|
<SDNumberInput
|
||||||
onChange={(v) => dispatch(setImg2imgStrength(Number(v)))}
|
label="Strength"
|
||||||
value={img2imgStrength}
|
step={0.01}
|
||||||
/>
|
min={0}
|
||||||
<SDSwitch
|
max={1}
|
||||||
isDisabled={!initialImagePath}
|
onChange={handleChangeStrength}
|
||||||
label='Fit initial image to output size'
|
value={img2imgStrength}
|
||||||
isChecked={shouldFitToWidthHeight}
|
/>
|
||||||
onChange={(e) =>
|
<SDSwitch
|
||||||
dispatch(setShouldFitToWidthHeight(e.target.checked))
|
label="Fit initial image to output size"
|
||||||
}
|
isChecked={shouldFitToWidthHeight}
|
||||||
/>
|
onChange={handleChangeFit}
|
||||||
<InitImage />
|
/>
|
||||||
</Flex>
|
<InitAndMaskImage />
|
||||||
);
|
</Flex>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ImageToImageOptions;
|
export default ImageToImageOptions;
|
||||||
|
63
frontend/src/features/sd/ImageUploader.tsx
Normal file
63
frontend/src/features/sd/ImageUploader.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
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 (
|
||||||
|
<div {...getRootProps()}>
|
||||||
|
<input {...getInputProps({ multiple: false })} />
|
||||||
|
{cloneElement(children, {
|
||||||
|
onClick: handleClickUploadIcon,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImageUploader;
|
57
frontend/src/features/sd/InitAndMaskImage.tsx
Normal file
57
frontend/src/features/sd/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 { SDState } from '../../features/sd/sdSlice';
|
||||||
|
import './InitAndMaskImage.css';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
|
import InitAndMaskUploadButtons from './InitAndMaskUploadButtons';
|
||||||
|
|
||||||
|
const sdSelector = createSelector(
|
||||||
|
(state: RootState) => state.sd,
|
||||||
|
(sd: SDState) => {
|
||||||
|
return {
|
||||||
|
initialImagePath: sd.initialImagePath,
|
||||||
|
maskPath: sd.maskPath,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ memoizeOptions: { resultEqualityCheck: isEqual } }
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays init and mask images and buttons to upload/delete them.
|
||||||
|
*/
|
||||||
|
const InitAndMaskImage = () => {
|
||||||
|
const { initialImagePath, maskPath } = useAppSelector(sdSelector);
|
||||||
|
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;
|
131
frontend/src/features/sd/InitAndMaskUploadButtons.tsx
Normal file
131
frontend/src/features/sd/InitAndMaskUploadButtons.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { Button, Flex, IconButton, useToast } from '@chakra-ui/react';
|
||||||
|
import { SyntheticEvent, useCallback } from 'react';
|
||||||
|
import { FaTrash } from 'react-icons/fa';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
|
import { RootState } from '../../app/store';
|
||||||
|
import {
|
||||||
|
SDState,
|
||||||
|
setInitialImagePath,
|
||||||
|
setMaskPath,
|
||||||
|
} from '../../features/sd/sdSlice';
|
||||||
|
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 sdSelector = createSelector(
|
||||||
|
(state: RootState) => state.sd,
|
||||||
|
(sd: SDState) => {
|
||||||
|
return {
|
||||||
|
initialImagePath: sd.initialImagePath,
|
||||||
|
maskPath: sd.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 } = useAppSelector(sdSelector);
|
||||||
|
|
||||||
|
// Use a toast to alert user when a file upload is rejected
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
// Clear the init and mask images
|
||||||
|
const handleClickResetInitialImageAndMask = (e: SyntheticEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
dispatch(setInitialImagePath(''));
|
||||||
|
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}
|
||||||
|
>
|
||||||
|
Upload Image
|
||||||
|
</Button>
|
||||||
|
</ImageUploader>
|
||||||
|
|
||||||
|
<ImageUploader
|
||||||
|
fileAcceptedCallback={maskImageFileAcceptedCallback}
|
||||||
|
fileRejectionCallback={fileRejectionCallback}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
isDisabled={!initialImagePath}
|
||||||
|
size={'sm'}
|
||||||
|
fontSize={'md'}
|
||||||
|
fontWeight={'normal'}
|
||||||
|
onMouseOver={handleMouseOverMaskUploadButton}
|
||||||
|
onMouseOut={handleMouseOutMaskUploadButton}
|
||||||
|
>
|
||||||
|
Upload Mask
|
||||||
|
</Button>
|
||||||
|
</ImageUploader>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
isDisabled={!initialImagePath}
|
||||||
|
size={'sm'}
|
||||||
|
aria-label={'Reset initial image and mask'}
|
||||||
|
onClick={handleClickResetInitialImageAndMask}
|
||||||
|
icon={<FaTrash />}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InitAndMaskUploadButtons;
|
@ -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,23 +1,24 @@
|
|||||||
import {
|
import {
|
||||||
Flex,
|
Flex,
|
||||||
Box,
|
Box,
|
||||||
Text,
|
Text,
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionItem,
|
AccordionItem,
|
||||||
AccordionButton,
|
AccordionButton,
|
||||||
AccordionIcon,
|
AccordionIcon,
|
||||||
AccordionPanel,
|
AccordionPanel,
|
||||||
Switch,
|
Switch,
|
||||||
|
ExpandedIndex,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from '../../app/store';
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
setShouldRunGFPGAN,
|
setShouldRunGFPGAN,
|
||||||
setShouldRunESRGAN,
|
setShouldRunESRGAN,
|
||||||
SDState,
|
SDState,
|
||||||
setShouldUseInitImage,
|
setShouldUseInitImage,
|
||||||
} from '../sd/sdSlice';
|
} from '../sd/sdSlice';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
@ -28,184 +29,189 @@ import ESRGANOptions from './ESRGANOptions';
|
|||||||
import GFPGANOptions from './GFPGANOptions';
|
import GFPGANOptions from './GFPGANOptions';
|
||||||
import OutputOptions from './OutputOptions';
|
import OutputOptions from './OutputOptions';
|
||||||
import ImageToImageOptions from './ImageToImageOptions';
|
import ImageToImageOptions from './ImageToImageOptions';
|
||||||
|
import { ChangeEvent } from 'react';
|
||||||
|
|
||||||
const sdSelector = createSelector(
|
const sdSelector = createSelector(
|
||||||
(state: RootState) => state.sd,
|
(state: RootState) => state.sd,
|
||||||
(sd: SDState) => {
|
(sd: SDState) => {
|
||||||
return {
|
return {
|
||||||
initialImagePath: sd.initialImagePath,
|
initialImagePath: sd.initialImagePath,
|
||||||
shouldUseInitImage: sd.shouldUseInitImage,
|
shouldUseInitImage: sd.shouldUseInitImage,
|
||||||
shouldRunESRGAN: sd.shouldRunESRGAN,
|
shouldRunESRGAN: sd.shouldRunESRGAN,
|
||||||
shouldRunGFPGAN: sd.shouldRunGFPGAN,
|
shouldRunGFPGAN: sd.shouldRunGFPGAN,
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: isEqual,
|
||||||
},
|
},
|
||||||
{
|
}
|
||||||
memoizeOptions: {
|
|
||||||
resultEqualityCheck: isEqual,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const systemSelector = createSelector(
|
const systemSelector = createSelector(
|
||||||
(state: RootState) => state.system,
|
(state: RootState) => state.system,
|
||||||
(system: SystemState) => {
|
(system: SystemState) => {
|
||||||
return {
|
return {
|
||||||
isGFPGANAvailable: system.isGFPGANAvailable,
|
isGFPGANAvailable: system.isGFPGANAvailable,
|
||||||
isESRGANAvailable: system.isESRGANAvailable,
|
isESRGANAvailable: system.isESRGANAvailable,
|
||||||
openAccordions: system.openAccordions,
|
openAccordions: system.openAccordions,
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: isEqual,
|
||||||
},
|
},
|
||||||
{
|
}
|
||||||
memoizeOptions: {
|
|
||||||
resultEqualityCheck: isEqual,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main container for generation and processing parameters.
|
||||||
|
*/
|
||||||
const OptionsAccordion = () => {
|
const OptionsAccordion = () => {
|
||||||
const {
|
const {
|
||||||
shouldRunESRGAN,
|
shouldRunESRGAN,
|
||||||
shouldRunGFPGAN,
|
shouldRunGFPGAN,
|
||||||
shouldUseInitImage,
|
shouldUseInitImage,
|
||||||
initialImagePath,
|
initialImagePath,
|
||||||
} = useAppSelector(sdSelector);
|
} = useAppSelector(sdSelector);
|
||||||
|
|
||||||
const { isGFPGANAvailable, isESRGANAvailable, openAccordions } =
|
const { isGFPGANAvailable, isESRGANAvailable, openAccordions } =
|
||||||
useAppSelector(systemSelector);
|
useAppSelector(systemSelector);
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
return (
|
/**
|
||||||
<Accordion
|
* Stores accordion state in redux so preferred UI setup is retained.
|
||||||
defaultIndex={openAccordions}
|
*/
|
||||||
allowMultiple
|
const handleChangeAccordionState = (openAccordions: ExpandedIndex) =>
|
||||||
reduceMotion
|
dispatch(setOpenAccordions(openAccordions));
|
||||||
onChange={(openAccordions) =>
|
|
||||||
dispatch(setOpenAccordions(openAccordions))
|
const handleChangeShouldRunESRGAN = (e: ChangeEvent<HTMLInputElement>) =>
|
||||||
}
|
dispatch(setShouldRunESRGAN(e.target.checked));
|
||||||
>
|
|
||||||
<AccordionItem>
|
const handleChangeShouldRunGFPGAN = (e: ChangeEvent<HTMLInputElement>) =>
|
||||||
<h2>
|
dispatch(setShouldRunGFPGAN(e.target.checked));
|
||||||
<AccordionButton>
|
|
||||||
<Box flex='1' textAlign='left'>
|
const handleChangeShouldUseInitImage = (e: ChangeEvent<HTMLInputElement>) =>
|
||||||
Seed & Variation
|
dispatch(setShouldUseInitImage(e.target.checked));
|
||||||
</Box>
|
|
||||||
<AccordionIcon />
|
return (
|
||||||
</AccordionButton>
|
<Accordion
|
||||||
</h2>
|
defaultIndex={openAccordions}
|
||||||
<AccordionPanel>
|
allowMultiple
|
||||||
<SeedVariationOptions />
|
reduceMotion
|
||||||
</AccordionPanel>
|
onChange={handleChangeAccordionState}
|
||||||
</AccordionItem>
|
>
|
||||||
<AccordionItem>
|
<AccordionItem>
|
||||||
<h2>
|
<h2>
|
||||||
<AccordionButton>
|
<AccordionButton>
|
||||||
<Box flex='1' textAlign='left'>
|
<Box flex="1" textAlign="left">
|
||||||
Sampler
|
Seed & Variation
|
||||||
</Box>
|
</Box>
|
||||||
<AccordionIcon />
|
<AccordionIcon />
|
||||||
</AccordionButton>
|
</AccordionButton>
|
||||||
</h2>
|
</h2>
|
||||||
<AccordionPanel>
|
<AccordionPanel>
|
||||||
<SamplerOptions />
|
<SeedVariationOptions />
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem>
|
<AccordionItem>
|
||||||
<h2>
|
<h2>
|
||||||
<AccordionButton>
|
<AccordionButton>
|
||||||
<Flex
|
<Box flex="1" textAlign="left">
|
||||||
justifyContent={'space-between'}
|
Sampler
|
||||||
alignItems={'center'}
|
</Box>
|
||||||
width={'100%'}
|
<AccordionIcon />
|
||||||
mr={2}
|
</AccordionButton>
|
||||||
>
|
</h2>
|
||||||
<Text>Upscale (ESRGAN)</Text>
|
<AccordionPanel>
|
||||||
<Switch
|
<SamplerOptions />
|
||||||
isDisabled={!isESRGANAvailable}
|
</AccordionPanel>
|
||||||
isChecked={shouldRunESRGAN}
|
</AccordionItem>
|
||||||
onChange={(e) =>
|
<AccordionItem>
|
||||||
dispatch(
|
<h2>
|
||||||
setShouldRunESRGAN(e.target.checked)
|
<AccordionButton>
|
||||||
)
|
<Flex
|
||||||
}
|
justifyContent={'space-between'}
|
||||||
/>
|
alignItems={'center'}
|
||||||
</Flex>
|
width={'100%'}
|
||||||
<AccordionIcon />
|
mr={2}
|
||||||
</AccordionButton>
|
>
|
||||||
</h2>
|
<Text>Upscale (ESRGAN)</Text>
|
||||||
<AccordionPanel>
|
<Switch
|
||||||
<ESRGANOptions />
|
isDisabled={!isESRGANAvailable}
|
||||||
</AccordionPanel>
|
isChecked={shouldRunESRGAN}
|
||||||
</AccordionItem>
|
onChange={handleChangeShouldRunESRGAN}
|
||||||
<AccordionItem>
|
/>
|
||||||
<h2>
|
</Flex>
|
||||||
<AccordionButton>
|
<AccordionIcon />
|
||||||
<Flex
|
</AccordionButton>
|
||||||
justifyContent={'space-between'}
|
</h2>
|
||||||
alignItems={'center'}
|
<AccordionPanel>
|
||||||
width={'100%'}
|
<ESRGANOptions />
|
||||||
mr={2}
|
</AccordionPanel>
|
||||||
>
|
</AccordionItem>
|
||||||
<Text>Fix Faces (GFPGAN)</Text>
|
<AccordionItem>
|
||||||
<Switch
|
<h2>
|
||||||
isDisabled={!isGFPGANAvailable}
|
<AccordionButton>
|
||||||
isChecked={shouldRunGFPGAN}
|
<Flex
|
||||||
onChange={(e) =>
|
justifyContent={'space-between'}
|
||||||
dispatch(
|
alignItems={'center'}
|
||||||
setShouldRunGFPGAN(e.target.checked)
|
width={'100%'}
|
||||||
)
|
mr={2}
|
||||||
}
|
>
|
||||||
/>
|
<Text>Fix Faces (GFPGAN)</Text>
|
||||||
</Flex>
|
<Switch
|
||||||
<AccordionIcon />
|
isDisabled={!isGFPGANAvailable}
|
||||||
</AccordionButton>
|
isChecked={shouldRunGFPGAN}
|
||||||
</h2>
|
onChange={handleChangeShouldRunGFPGAN}
|
||||||
<AccordionPanel>
|
/>
|
||||||
<GFPGANOptions />
|
</Flex>
|
||||||
</AccordionPanel>
|
<AccordionIcon />
|
||||||
</AccordionItem>
|
</AccordionButton>
|
||||||
<AccordionItem>
|
</h2>
|
||||||
<h2>
|
<AccordionPanel>
|
||||||
<AccordionButton>
|
<GFPGANOptions />
|
||||||
<Flex
|
</AccordionPanel>
|
||||||
justifyContent={'space-between'}
|
</AccordionItem>
|
||||||
alignItems={'center'}
|
<AccordionItem>
|
||||||
width={'100%'}
|
<h2>
|
||||||
mr={2}
|
<AccordionButton>
|
||||||
>
|
<Flex
|
||||||
<Text>Image to Image</Text>
|
justifyContent={'space-between'}
|
||||||
<Switch
|
alignItems={'center'}
|
||||||
isDisabled={!initialImagePath}
|
width={'100%'}
|
||||||
isChecked={shouldUseInitImage}
|
mr={2}
|
||||||
onChange={(e) =>
|
>
|
||||||
dispatch(
|
<Text>Image to Image</Text>
|
||||||
setShouldUseInitImage(e.target.checked)
|
<Switch
|
||||||
)
|
isDisabled={!initialImagePath}
|
||||||
}
|
isChecked={shouldUseInitImage}
|
||||||
/>
|
onChange={handleChangeShouldUseInitImage}
|
||||||
</Flex>
|
/>
|
||||||
<AccordionIcon />
|
</Flex>
|
||||||
</AccordionButton>
|
<AccordionIcon />
|
||||||
</h2>
|
</AccordionButton>
|
||||||
<AccordionPanel>
|
</h2>
|
||||||
<ImageToImageOptions />
|
<AccordionPanel>
|
||||||
</AccordionPanel>
|
<ImageToImageOptions />
|
||||||
</AccordionItem>
|
</AccordionPanel>
|
||||||
<AccordionItem>
|
</AccordionItem>
|
||||||
<h2>
|
<AccordionItem>
|
||||||
<AccordionButton>
|
<h2>
|
||||||
<Box flex='1' textAlign='left'>
|
<AccordionButton>
|
||||||
Output
|
<Box flex="1" textAlign="left">
|
||||||
</Box>
|
Output
|
||||||
<AccordionIcon />
|
</Box>
|
||||||
</AccordionButton>
|
<AccordionIcon />
|
||||||
</h2>
|
</AccordionButton>
|
||||||
<AccordionPanel>
|
</h2>
|
||||||
<OutputOptions />
|
<AccordionPanel>
|
||||||
</AccordionPanel>
|
<OutputOptions />
|
||||||
</AccordionItem>
|
</AccordionPanel>
|
||||||
</Accordion>
|
</AccordionItem>
|
||||||
);
|
</Accordion>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default OptionsAccordion;
|
export default OptionsAccordion;
|
||||||
|
@ -1,66 +1,76 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex } from '@chakra-ui/react';
|
||||||
|
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from '../../app/store';
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
|
|
||||||
import { setHeight, setWidth, setSeamless, SDState } from '../sd/sdSlice';
|
import { setHeight, setWidth, setSeamless, SDState } from '../sd/sdSlice';
|
||||||
|
|
||||||
import SDSelect from '../../components/SDSelect';
|
|
||||||
|
|
||||||
import { HEIGHTS, WIDTHS } from '../../app/constants';
|
import { HEIGHTS, WIDTHS } from '../../app/constants';
|
||||||
import SDSwitch from '../../components/SDSwitch';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
|
import { ChangeEvent } from 'react';
|
||||||
|
import SDSelect from '../../common/components/SDSelect';
|
||||||
|
import SDSwitch from '../../common/components/SDSwitch';
|
||||||
|
|
||||||
const sdSelector = createSelector(
|
const sdSelector = createSelector(
|
||||||
(state: RootState) => state.sd,
|
(state: RootState) => state.sd,
|
||||||
(sd: SDState) => {
|
(sd: SDState) => {
|
||||||
return {
|
return {
|
||||||
height: sd.height,
|
height: sd.height,
|
||||||
width: sd.width,
|
width: sd.width,
|
||||||
seamless: sd.seamless,
|
seamless: sd.seamless,
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: isEqual,
|
||||||
},
|
},
|
||||||
{
|
}
|
||||||
memoizeOptions: {
|
|
||||||
resultEqualityCheck: isEqual,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image output options. Includes width, height, seamless tiling.
|
||||||
|
*/
|
||||||
const OutputOptions = () => {
|
const OutputOptions = () => {
|
||||||
const { height, width, seamless } = useAppSelector(sdSelector);
|
const dispatch = useAppDispatch();
|
||||||
|
const { height, width, seamless } = useAppSelector(sdSelector);
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const handleChangeWidth = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||||
|
dispatch(setWidth(Number(e.target.value)));
|
||||||
|
|
||||||
return (
|
const handleChangeHeight = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||||
<Flex gap={2} direction={'column'}>
|
dispatch(setHeight(Number(e.target.value)));
|
||||||
<Flex gap={2}>
|
|
||||||
<SDSelect
|
const handleChangeSeamless = (e: ChangeEvent<HTMLInputElement>) =>
|
||||||
label='Width'
|
dispatch(setSeamless(e.target.checked));
|
||||||
value={width}
|
|
||||||
flexGrow={1}
|
return (
|
||||||
onChange={(e) => dispatch(setWidth(Number(e.target.value)))}
|
<Flex gap={2} direction={'column'}>
|
||||||
validValues={WIDTHS}
|
<Flex gap={2}>
|
||||||
/>
|
<SDSelect
|
||||||
<SDSelect
|
label="Width"
|
||||||
label='Height'
|
value={width}
|
||||||
value={height}
|
flexGrow={1}
|
||||||
flexGrow={1}
|
onChange={handleChangeWidth}
|
||||||
onChange={(e) =>
|
validValues={WIDTHS}
|
||||||
dispatch(setHeight(Number(e.target.value)))
|
/>
|
||||||
}
|
<SDSelect
|
||||||
validValues={HEIGHTS}
|
label="Height"
|
||||||
/>
|
value={height}
|
||||||
</Flex>
|
flexGrow={1}
|
||||||
<SDSwitch
|
onChange={handleChangeHeight}
|
||||||
label='Seamless tiling'
|
validValues={HEIGHTS}
|
||||||
fontSize={'md'}
|
/>
|
||||||
isChecked={seamless}
|
</Flex>
|
||||||
onChange={(e) => dispatch(setSeamless(e.target.checked))}
|
<SDSwitch
|
||||||
/>
|
label="Seamless tiling"
|
||||||
</Flex>
|
fontSize={'md'}
|
||||||
);
|
isChecked={seamless}
|
||||||
|
onChange={handleChangeSeamless}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default OutputOptions;
|
export default OutputOptions;
|
||||||
|
@ -1,58 +1,68 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
import { cancelProcessing, generateImage } from '../../app/socketio';
|
import { cancelProcessing, generateImage } from '../../app/socketio/actions';
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from '../../app/store';
|
||||||
import SDButton from '../../components/SDButton';
|
import SDButton from '../../common/components/SDButton';
|
||||||
|
import useCheckParameters from '../../common/hooks/useCheckParameters';
|
||||||
import { SystemState } from '../system/systemSlice';
|
import { SystemState } from '../system/systemSlice';
|
||||||
import useCheckParameters from '../system/useCheckParameters';
|
|
||||||
|
|
||||||
const systemSelector = createSelector(
|
const systemSelector = createSelector(
|
||||||
(state: RootState) => state.system,
|
(state: RootState) => state.system,
|
||||||
(system: SystemState) => {
|
(system: SystemState) => {
|
||||||
return {
|
return {
|
||||||
isProcessing: system.isProcessing,
|
isProcessing: system.isProcessing,
|
||||||
isConnected: system.isConnected,
|
isConnected: system.isConnected,
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: isEqual,
|
||||||
},
|
},
|
||||||
{
|
}
|
||||||
memoizeOptions: {
|
|
||||||
resultEqualityCheck: isEqual,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buttons to start and cancel image generation.
|
||||||
|
*/
|
||||||
const ProcessButtons = () => {
|
const ProcessButtons = () => {
|
||||||
const { isProcessing, isConnected } = useAppSelector(systemSelector);
|
const dispatch = useAppDispatch();
|
||||||
|
const { isProcessing, isConnected } = useAppSelector(systemSelector);
|
||||||
|
const isReady = useCheckParameters();
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const handleClickGenerate = () => dispatch(generateImage());
|
||||||
|
|
||||||
const isReady = useCheckParameters();
|
const handleClickCancel = () => dispatch(cancelProcessing());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap={2} direction={'column'} alignItems={'space-between'} height={'100%'}>
|
<Flex
|
||||||
<SDButton
|
gap={2}
|
||||||
label='Generate'
|
direction={'column'}
|
||||||
type='submit'
|
alignItems={'space-between'}
|
||||||
colorScheme='green'
|
height={'100%'}
|
||||||
flexGrow={1}
|
>
|
||||||
isDisabled={!isReady}
|
<SDButton
|
||||||
fontSize={'md'}
|
label="Generate"
|
||||||
size={'md'}
|
type="submit"
|
||||||
onClick={() => dispatch(generateImage())}
|
colorScheme="green"
|
||||||
/>
|
flexGrow={1}
|
||||||
<SDButton
|
isDisabled={!isReady}
|
||||||
label='Cancel'
|
fontSize={'md'}
|
||||||
colorScheme='red'
|
size={'md'}
|
||||||
flexGrow={1}
|
onClick={handleClickGenerate}
|
||||||
fontSize={'md'}
|
/>
|
||||||
size={'md'}
|
<SDButton
|
||||||
isDisabled={!isConnected || !isProcessing}
|
label="Cancel"
|
||||||
onClick={() => dispatch(cancelProcessing())}
|
colorScheme="red"
|
||||||
/>
|
flexGrow={1}
|
||||||
</Flex>
|
fontSize={'md'}
|
||||||
);
|
size={'md'}
|
||||||
|
isDisabled={!isConnected || !isProcessing}
|
||||||
|
onClick={handleClickCancel}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProcessButtons;
|
export default ProcessButtons;
|
||||||
|
@ -1,21 +1,40 @@
|
|||||||
import { Textarea } from '@chakra-ui/react';
|
import { Textarea } from '@chakra-ui/react';
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
import {
|
||||||
|
ChangeEvent,
|
||||||
|
KeyboardEvent,
|
||||||
|
} from 'react';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
|
import { generateImage } from '../../app/socketio/actions';
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from '../../app/store';
|
||||||
import { setPrompt } from '../sd/sdSlice';
|
import { setPrompt } from '../sd/sdSlice';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompt input text area.
|
||||||
|
*/
|
||||||
const PromptInput = () => {
|
const PromptInput = () => {
|
||||||
const { prompt } = useAppSelector((state: RootState) => state.sd);
|
const { prompt } = useAppSelector((state: RootState) => state.sd);
|
||||||
const dispatch = useAppDispatch();
|
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 (
|
return (
|
||||||
<Textarea
|
<Textarea
|
||||||
id='prompt'
|
id="prompt"
|
||||||
name='prompt'
|
name="prompt"
|
||||||
resize='none'
|
resize="none"
|
||||||
size={'lg'}
|
size={'lg'}
|
||||||
height={'100%'}
|
height={'100%'}
|
||||||
isInvalid={!prompt.length}
|
isInvalid={!prompt.length}
|
||||||
onChange={(e) => dispatch(setPrompt(e.target.value))}
|
onChange={handleChangePrompt}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
value={prompt}
|
value={prompt}
|
||||||
placeholder="I'm dreaming of..."
|
placeholder="I'm dreaming of..."
|
||||||
/>
|
/>
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
import {
|
|
||||||
Slider,
|
|
||||||
SliderTrack,
|
|
||||||
SliderFilledTrack,
|
|
||||||
SliderThumb,
|
|
||||||
FormControl,
|
|
||||||
FormLabel,
|
|
||||||
Text,
|
|
||||||
Flex,
|
|
||||||
SliderProps,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
|
|
||||||
interface Props extends SliderProps {
|
|
||||||
label: string;
|
|
||||||
value: number;
|
|
||||||
fontSize?: number | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SDSlider = ({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
fontSize = 'sm',
|
|
||||||
onChange,
|
|
||||||
...rest
|
|
||||||
}: Props) => {
|
|
||||||
return (
|
|
||||||
<FormControl>
|
|
||||||
<Flex gap={2}>
|
|
||||||
<FormLabel marginInlineEnd={0} marginBottom={1}>
|
|
||||||
<Text fontSize={fontSize} whiteSpace='nowrap'>
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
</FormLabel>
|
|
||||||
<Slider
|
|
||||||
aria-label={label}
|
|
||||||
focusThumbOnChange={true}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
<SliderTrack>
|
|
||||||
<SliderFilledTrack />
|
|
||||||
</SliderTrack>
|
|
||||||
<SliderThumb />
|
|
||||||
</Slider>
|
|
||||||
</Flex>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SDSlider;
|
|
@ -1,79 +1,98 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex } from '@chakra-ui/react';
|
||||||
|
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from '../../app/store';
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
|
|
||||||
import { setCfgScale, setSampler, setThreshold, setPerlin, setSteps, SDState } from '../sd/sdSlice';
|
import { setCfgScale, setSampler, setThreshold, setPerlin, setSteps, SDState } from '../sd/sdSlice';
|
||||||
|
|
||||||
import SDNumberInput from '../../components/SDNumberInput';
|
|
||||||
import SDSelect from '../../components/SDSelect';
|
|
||||||
|
|
||||||
import { SAMPLERS } from '../../app/constants';
|
import { SAMPLERS } from '../../app/constants';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
|
import { ChangeEvent } from 'react';
|
||||||
|
import SDNumberInput from '../../common/components/SDNumberInput';
|
||||||
|
import SDSelect from '../../common/components/SDSelect';
|
||||||
|
|
||||||
const sdSelector = createSelector(
|
const sdSelector = createSelector(
|
||||||
(state: RootState) => state.sd,
|
(state: RootState) => state.sd,
|
||||||
(sd: SDState) => {
|
(sd: SDState) => {
|
||||||
return {
|
return {
|
||||||
steps: sd.steps,
|
steps: sd.steps,
|
||||||
cfgScale: sd.cfgScale,
|
cfgScale: sd.cfgScale,
|
||||||
sampler: sd.sampler,
|
sampler: sd.sampler,
|
||||||
threshold: sd.threshold,
|
threshold: sd.threshold,
|
||||||
perlin: sd.perlin,
|
perlin: sd.perlin,
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: isEqual,
|
||||||
},
|
},
|
||||||
{
|
}
|
||||||
memoizeOptions: {
|
|
||||||
resultEqualityCheck: isEqual,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sampler options. Includes steps, CFG scale, sampler.
|
||||||
|
*/
|
||||||
const SamplerOptions = () => {
|
const SamplerOptions = () => {
|
||||||
const { steps, cfgScale, sampler, threshold, perlin } = useAppSelector(sdSelector);
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const { steps, cfgScale, sampler, threshold, perlin } = useAppSelector(sdSelector);
|
||||||
|
|
||||||
return (
|
const handleChangeSteps = (v: string | number) =>
|
||||||
<Flex gap={2} direction={'column'}>
|
dispatch(setSteps(Number(v)));
|
||||||
<SDNumberInput
|
|
||||||
label='Steps'
|
const handleChangeCfgScale = (v: string | number) =>
|
||||||
min={1}
|
dispatch(setCfgScale(Number(v)));
|
||||||
step={1}
|
|
||||||
precision={0}
|
const handleChangeSampler = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||||
onChange={(v) => dispatch(setSteps(Number(v)))}
|
dispatch(setSampler(e.target.value));
|
||||||
value={steps}
|
|
||||||
/>
|
const handleChangeThreshold = (v: string | number) =>
|
||||||
<SDNumberInput
|
dispatch(setThreshold(Number(v)));
|
||||||
label='CFG scale'
|
|
||||||
step={0.5}
|
const handleChangePerlin = (v: string | number) =>
|
||||||
onChange={(v) => dispatch(setCfgScale(Number(v)))}
|
dispatch(setPerlin(Number(v)));
|
||||||
value={cfgScale}
|
|
||||||
/>
|
return (
|
||||||
<SDSelect
|
<Flex gap={2} direction={'column'}>
|
||||||
label='Sampler'
|
<SDNumberInput
|
||||||
value={sampler}
|
label="Steps"
|
||||||
onChange={(e) => dispatch(setSampler(e.target.value))}
|
min={1}
|
||||||
validValues={SAMPLERS}
|
step={1}
|
||||||
/>
|
precision={0}
|
||||||
<SDNumberInput
|
onChange={handleChangeSteps}
|
||||||
label='Threshold'
|
value={steps}
|
||||||
min={0}
|
/>
|
||||||
step={0.1}
|
<SDNumberInput
|
||||||
onChange={(v) => dispatch(setThreshold(Number(v)))}
|
label="CFG scale"
|
||||||
value={threshold}
|
step={0.5}
|
||||||
/>
|
onChange={handleChangeCfgScale}
|
||||||
<SDNumberInput
|
value={cfgScale}
|
||||||
label='Perlin'
|
/>
|
||||||
min={0}
|
<SDSelect
|
||||||
max={1}
|
label="Sampler"
|
||||||
step={0.05}
|
value={sampler}
|
||||||
onChange={(v) => dispatch(setPerlin(Number(v)))}
|
onChange={handleChangeSampler}
|
||||||
value={perlin}
|
validValues={SAMPLERS}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
<SDNumberInput
|
||||||
);
|
label='Threshold'
|
||||||
|
min={0}
|
||||||
|
step={0.1}
|
||||||
|
onChange={handleChangeThreshold}
|
||||||
|
value={threshold}
|
||||||
|
/>
|
||||||
|
<SDNumberInput
|
||||||
|
label='Perlin'
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.05}
|
||||||
|
onChange={handleChangePerlin}
|
||||||
|
value={perlin}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SamplerOptions;
|
export default SamplerOptions;
|
||||||
|
@ -1,144 +1,159 @@
|
|||||||
import {
|
import {
|
||||||
Flex,
|
Flex,
|
||||||
Input,
|
Input,
|
||||||
HStack,
|
HStack,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
Text,
|
Text,
|
||||||
Button,
|
Button,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
|
import { ChangeEvent } from 'react';
|
||||||
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from '../../app/constants';
|
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from '../../app/constants';
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from '../../app/store';
|
||||||
import SDNumberInput from '../../components/SDNumberInput';
|
import SDNumberInput from '../../common/components/SDNumberInput';
|
||||||
import SDSwitch from '../../components/SDSwitch';
|
import SDSwitch from '../../common/components/SDSwitch';
|
||||||
|
import randomInt from '../../common/util/randomInt';
|
||||||
|
import { validateSeedWeights } from '../../common/util/seedWeightPairs';
|
||||||
import {
|
import {
|
||||||
randomizeSeed,
|
SDState,
|
||||||
SDState,
|
setIterations,
|
||||||
setIterations,
|
setSeed,
|
||||||
setSeed,
|
setSeedWeights,
|
||||||
setSeedWeights,
|
setShouldGenerateVariations,
|
||||||
setShouldGenerateVariations,
|
setShouldRandomizeSeed,
|
||||||
setShouldRandomizeSeed,
|
setVariationAmount,
|
||||||
setVariantAmount,
|
|
||||||
} from './sdSlice';
|
} from './sdSlice';
|
||||||
import { validateSeedWeights } from './util/seedWeightPairs';
|
|
||||||
|
|
||||||
const sdSelector = createSelector(
|
const sdSelector = createSelector(
|
||||||
(state: RootState) => state.sd,
|
(state: RootState) => state.sd,
|
||||||
(sd: SDState) => {
|
(sd: SDState) => {
|
||||||
return {
|
return {
|
||||||
variantAmount: sd.variantAmount,
|
variationAmount: sd.variationAmount,
|
||||||
seedWeights: sd.seedWeights,
|
seedWeights: sd.seedWeights,
|
||||||
shouldGenerateVariations: sd.shouldGenerateVariations,
|
shouldGenerateVariations: sd.shouldGenerateVariations,
|
||||||
shouldRandomizeSeed: sd.shouldRandomizeSeed,
|
shouldRandomizeSeed: sd.shouldRandomizeSeed,
|
||||||
seed: sd.seed,
|
seed: sd.seed,
|
||||||
iterations: sd.iterations,
|
iterations: sd.iterations,
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: isEqual,
|
||||||
},
|
},
|
||||||
{
|
}
|
||||||
memoizeOptions: {
|
|
||||||
resultEqualityCheck: isEqual,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed & variation options. Includes iteration, seed, seed randomization, variation options.
|
||||||
|
*/
|
||||||
const SeedVariationOptions = () => {
|
const SeedVariationOptions = () => {
|
||||||
const {
|
const {
|
||||||
shouldGenerateVariations,
|
shouldGenerateVariations,
|
||||||
variantAmount,
|
variationAmount,
|
||||||
seedWeights,
|
seedWeights,
|
||||||
shouldRandomizeSeed,
|
shouldRandomizeSeed,
|
||||||
seed,
|
seed,
|
||||||
iterations,
|
iterations,
|
||||||
} = useAppSelector(sdSelector);
|
} = useAppSelector(sdSelector);
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
return (
|
const handleChangeIterations = (v: string | number) =>
|
||||||
<Flex gap={2} direction={'column'}>
|
dispatch(setIterations(Number(v)));
|
||||||
<SDNumberInput
|
|
||||||
label='Images to generate'
|
const handleChangeShouldRandomizeSeed = (e: ChangeEvent<HTMLInputElement>) =>
|
||||||
step={1}
|
dispatch(setShouldRandomizeSeed(e.target.checked));
|
||||||
min={1}
|
|
||||||
precision={0}
|
const handleChangeSeed = (v: string | number) => dispatch(setSeed(Number(v)));
|
||||||
onChange={(v) => dispatch(setIterations(Number(v)))}
|
|
||||||
value={iterations}
|
const handleClickRandomizeSeed = () =>
|
||||||
/>
|
dispatch(setSeed(randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX)));
|
||||||
<SDSwitch
|
|
||||||
label='Randomize seed on generation'
|
const handleChangeShouldGenerateVariations = (
|
||||||
isChecked={shouldRandomizeSeed}
|
e: ChangeEvent<HTMLInputElement>
|
||||||
onChange={(e) =>
|
) => dispatch(setShouldGenerateVariations(e.target.checked));
|
||||||
dispatch(setShouldRandomizeSeed(e.target.checked))
|
|
||||||
}
|
const handleChangevariationAmount = (v: string | number) =>
|
||||||
/>
|
dispatch(setVariationAmount(Number(v)));
|
||||||
<Flex gap={2}>
|
|
||||||
<SDNumberInput
|
const handleChangeSeedWeights = (e: ChangeEvent<HTMLInputElement>) =>
|
||||||
label='Seed'
|
dispatch(setSeedWeights(e.target.value));
|
||||||
step={1}
|
|
||||||
precision={0}
|
return (
|
||||||
flexGrow={1}
|
<Flex gap={2} direction={'column'}>
|
||||||
min={NUMPY_RAND_MIN}
|
<SDNumberInput
|
||||||
max={NUMPY_RAND_MAX}
|
label="Images to generate"
|
||||||
isDisabled={shouldRandomizeSeed}
|
step={1}
|
||||||
isInvalid={seed < 0 && shouldGenerateVariations}
|
min={1}
|
||||||
onChange={(v) => dispatch(setSeed(Number(v)))}
|
precision={0}
|
||||||
value={seed}
|
onChange={handleChangeIterations}
|
||||||
/>
|
value={iterations}
|
||||||
<Button
|
/>
|
||||||
size={'sm'}
|
<SDSwitch
|
||||||
isDisabled={shouldRandomizeSeed}
|
label="Randomize seed on generation"
|
||||||
onClick={() => dispatch(randomizeSeed())}
|
isChecked={shouldRandomizeSeed}
|
||||||
>
|
onChange={handleChangeShouldRandomizeSeed}
|
||||||
<Text pl={2} pr={2}>
|
/>
|
||||||
Shuffle
|
<Flex gap={2}>
|
||||||
</Text>
|
<SDNumberInput
|
||||||
</Button>
|
label="Seed"
|
||||||
</Flex>
|
step={1}
|
||||||
<SDSwitch
|
precision={0}
|
||||||
label='Generate variations'
|
flexGrow={1}
|
||||||
isChecked={shouldGenerateVariations}
|
min={NUMPY_RAND_MIN}
|
||||||
width={'auto'}
|
max={NUMPY_RAND_MAX}
|
||||||
onChange={(e) =>
|
isDisabled={shouldRandomizeSeed}
|
||||||
dispatch(setShouldGenerateVariations(e.target.checked))
|
isInvalid={seed < 0 && shouldGenerateVariations}
|
||||||
}
|
onChange={handleChangeSeed}
|
||||||
/>
|
value={seed}
|
||||||
<SDNumberInput
|
/>
|
||||||
label='Variation amount'
|
<Button
|
||||||
value={variantAmount}
|
size={'sm'}
|
||||||
step={0.01}
|
isDisabled={shouldRandomizeSeed}
|
||||||
min={0}
|
onClick={handleClickRandomizeSeed}
|
||||||
max={1}
|
>
|
||||||
isDisabled={!shouldGenerateVariations}
|
<Text pl={2} pr={2}>
|
||||||
onChange={(v) => dispatch(setVariantAmount(Number(v)))}
|
Shuffle
|
||||||
/>
|
</Text>
|
||||||
<FormControl
|
</Button>
|
||||||
isInvalid={
|
</Flex>
|
||||||
shouldGenerateVariations &&
|
<SDSwitch
|
||||||
!(validateSeedWeights(seedWeights) || seedWeights === '')
|
label="Generate variations"
|
||||||
}
|
isChecked={shouldGenerateVariations}
|
||||||
flexGrow={1}
|
width={'auto'}
|
||||||
isDisabled={!shouldGenerateVariations}
|
onChange={handleChangeShouldGenerateVariations}
|
||||||
>
|
/>
|
||||||
<HStack>
|
<SDNumberInput
|
||||||
<FormLabel marginInlineEnd={0} marginBottom={1}>
|
label="Variation amount"
|
||||||
<Text whiteSpace='nowrap'>
|
value={variationAmount}
|
||||||
Seed Weights
|
step={0.01}
|
||||||
</Text>
|
min={0}
|
||||||
</FormLabel>
|
max={1}
|
||||||
<Input
|
onChange={handleChangevariationAmount}
|
||||||
size={'sm'}
|
/>
|
||||||
value={seedWeights}
|
<FormControl
|
||||||
onChange={(e) =>
|
isInvalid={
|
||||||
dispatch(setSeedWeights(e.target.value))
|
shouldGenerateVariations &&
|
||||||
}
|
!(validateSeedWeights(seedWeights) || seedWeights === '')
|
||||||
/>
|
}
|
||||||
</HStack>
|
flexGrow={1}
|
||||||
</FormControl>
|
>
|
||||||
</Flex>
|
<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;
|
export default SeedVariationOptions;
|
||||||
|
@ -1,92 +0,0 @@
|
|||||||
import {
|
|
||||||
Flex,
|
|
||||||
FormControl,
|
|
||||||
FormLabel,
|
|
||||||
HStack,
|
|
||||||
Input,
|
|
||||||
Text,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { isEqual } from 'lodash';
|
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
|
||||||
import { RootState } from '../../app/store';
|
|
||||||
import SDNumberInput from '../../components/SDNumberInput';
|
|
||||||
import SDSwitch from '../../components/SDSwitch';
|
|
||||||
import {
|
|
||||||
SDState,
|
|
||||||
setSeedWeights,
|
|
||||||
setShouldGenerateVariations,
|
|
||||||
setVariantAmount,
|
|
||||||
} from './sdSlice';
|
|
||||||
import { validateSeedWeights } from './util/seedWeightPairs';
|
|
||||||
|
|
||||||
const sdSelector = createSelector(
|
|
||||||
(state: RootState) => state.sd,
|
|
||||||
(sd: SDState) => {
|
|
||||||
return {
|
|
||||||
variantAmount: sd.variantAmount,
|
|
||||||
seedWeights: sd.seedWeights,
|
|
||||||
shouldGenerateVariations: sd.shouldGenerateVariations,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memoizeOptions: {
|
|
||||||
resultEqualityCheck: isEqual,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const Variant = () => {
|
|
||||||
const { shouldGenerateVariations, variantAmount, seedWeights } =
|
|
||||||
useAppSelector(sdSelector);
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex gap={2} alignItems={'center'} pl={1}>
|
|
||||||
<SDSwitch
|
|
||||||
label='Generate variations'
|
|
||||||
isChecked={shouldGenerateVariations}
|
|
||||||
width={'auto'}
|
|
||||||
onChange={(e) =>
|
|
||||||
dispatch(setShouldGenerateVariations(e.target.checked))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<SDNumberInput
|
|
||||||
label='Amount'
|
|
||||||
value={variantAmount}
|
|
||||||
step={0.01}
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
width={240}
|
|
||||||
isDisabled={!shouldGenerateVariations}
|
|
||||||
onChange={(v) => dispatch(setVariantAmount(Number(v)))}
|
|
||||||
/>
|
|
||||||
<FormControl
|
|
||||||
isInvalid={
|
|
||||||
shouldGenerateVariations &&
|
|
||||||
!(validateSeedWeights(seedWeights) || seedWeights === '')
|
|
||||||
}
|
|
||||||
flexGrow={1}
|
|
||||||
isDisabled={!shouldGenerateVariations}
|
|
||||||
>
|
|
||||||
<HStack>
|
|
||||||
<FormLabel marginInlineEnd={0} marginBottom={1}>
|
|
||||||
<Text fontSize={'sm'} whiteSpace='nowrap'>
|
|
||||||
Seed Weights
|
|
||||||
</Text>
|
|
||||||
</FormLabel>
|
|
||||||
<Input
|
|
||||||
size={'sm'}
|
|
||||||
value={seedWeights}
|
|
||||||
onChange={(e) =>
|
|
||||||
dispatch(setSeedWeights(e.target.value))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
</FormControl>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Variant;
|
|
@ -1,24 +1,13 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { SDMetadata } from '../gallery/gallerySlice';
|
import { SDMetadata } from '../gallery/gallerySlice';
|
||||||
import randomInt from './util/randomInt';
|
|
||||||
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from '../../app/constants';
|
|
||||||
|
|
||||||
const calculateRealSteps = (
|
export type UpscalingLevel = 2 | 4;
|
||||||
steps: number,
|
|
||||||
strength: number,
|
|
||||||
hasInitImage: boolean
|
|
||||||
): number => {
|
|
||||||
return hasInitImage ? Math.floor(strength * steps) : steps;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UpscalingLevel = 0 | 2 | 3 | 4;
|
|
||||||
|
|
||||||
export interface SDState {
|
export interface SDState {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
iterations: number;
|
iterations: number;
|
||||||
steps: number;
|
steps: number;
|
||||||
realSteps: number;
|
|
||||||
cfgScale: number;
|
cfgScale: number;
|
||||||
height: number;
|
height: number;
|
||||||
width: number;
|
width: number;
|
||||||
@ -36,7 +25,7 @@ export interface SDState {
|
|||||||
seamless: boolean;
|
seamless: boolean;
|
||||||
shouldFitToWidthHeight: boolean;
|
shouldFitToWidthHeight: boolean;
|
||||||
shouldGenerateVariations: boolean;
|
shouldGenerateVariations: boolean;
|
||||||
variantAmount: number;
|
variationAmount: number;
|
||||||
seedWeights: string;
|
seedWeights: string;
|
||||||
shouldRunESRGAN: boolean;
|
shouldRunESRGAN: boolean;
|
||||||
shouldRunGFPGAN: boolean;
|
shouldRunGFPGAN: boolean;
|
||||||
@ -47,7 +36,6 @@ const initialSDState: SDState = {
|
|||||||
prompt: '',
|
prompt: '',
|
||||||
iterations: 1,
|
iterations: 1,
|
||||||
steps: 50,
|
steps: 50,
|
||||||
realSteps: 50,
|
|
||||||
cfgScale: 7.5,
|
cfgScale: 7.5,
|
||||||
height: 512,
|
height: 512,
|
||||||
width: 512,
|
width: 512,
|
||||||
@ -62,7 +50,7 @@ const initialSDState: SDState = {
|
|||||||
maskPath: '',
|
maskPath: '',
|
||||||
shouldFitToWidthHeight: true,
|
shouldFitToWidthHeight: true,
|
||||||
shouldGenerateVariations: false,
|
shouldGenerateVariations: false,
|
||||||
variantAmount: 0.1,
|
variationAmount: 0.1,
|
||||||
seedWeights: '',
|
seedWeights: '',
|
||||||
shouldRunESRGAN: false,
|
shouldRunESRGAN: false,
|
||||||
upscalingLevel: 4,
|
upscalingLevel: 4,
|
||||||
@ -85,14 +73,7 @@ export const sdSlice = createSlice({
|
|||||||
state.iterations = action.payload;
|
state.iterations = action.payload;
|
||||||
},
|
},
|
||||||
setSteps: (state, action: PayloadAction<number>) => {
|
setSteps: (state, action: PayloadAction<number>) => {
|
||||||
const { img2imgStrength, initialImagePath } = state;
|
state.steps = action.payload;
|
||||||
const steps = action.payload;
|
|
||||||
state.steps = steps;
|
|
||||||
state.realSteps = calculateRealSteps(
|
|
||||||
steps,
|
|
||||||
img2imgStrength,
|
|
||||||
Boolean(initialImagePath)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
setCfgScale: (state, action: PayloadAction<number>) => {
|
setCfgScale: (state, action: PayloadAction<number>) => {
|
||||||
state.cfgScale = action.payload;
|
state.cfgScale = action.payload;
|
||||||
@ -117,14 +98,7 @@ export const sdSlice = createSlice({
|
|||||||
state.shouldRandomizeSeed = false;
|
state.shouldRandomizeSeed = false;
|
||||||
},
|
},
|
||||||
setImg2imgStrength: (state, action: PayloadAction<number>) => {
|
setImg2imgStrength: (state, action: PayloadAction<number>) => {
|
||||||
const img2imgStrength = action.payload;
|
state.img2imgStrength = action.payload;
|
||||||
const { steps, initialImagePath } = state;
|
|
||||||
state.img2imgStrength = img2imgStrength;
|
|
||||||
state.realSteps = calculateRealSteps(
|
|
||||||
steps,
|
|
||||||
img2imgStrength,
|
|
||||||
Boolean(initialImagePath)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
setGfpganStrength: (state, action: PayloadAction<number>) => {
|
setGfpganStrength: (state, action: PayloadAction<number>) => {
|
||||||
state.gfpganStrength = action.payload;
|
state.gfpganStrength = action.payload;
|
||||||
@ -139,15 +113,9 @@ export const sdSlice = createSlice({
|
|||||||
state.shouldUseInitImage = action.payload;
|
state.shouldUseInitImage = action.payload;
|
||||||
},
|
},
|
||||||
setInitialImagePath: (state, action: PayloadAction<string>) => {
|
setInitialImagePath: (state, action: PayloadAction<string>) => {
|
||||||
const initialImagePath = action.payload;
|
const newInitialImagePath = action.payload;
|
||||||
const { steps, img2imgStrength } = state;
|
state.shouldUseInitImage = newInitialImagePath ? true : false;
|
||||||
state.shouldUseInitImage = initialImagePath ? true : false;
|
state.initialImagePath = newInitialImagePath;
|
||||||
state.initialImagePath = initialImagePath;
|
|
||||||
state.realSteps = calculateRealSteps(
|
|
||||||
steps,
|
|
||||||
img2imgStrength,
|
|
||||||
Boolean(initialImagePath)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
setMaskPath: (state, action: PayloadAction<string>) => {
|
setMaskPath: (state, action: PayloadAction<string>) => {
|
||||||
state.maskPath = action.payload;
|
state.maskPath = action.payload;
|
||||||
@ -161,13 +129,11 @@ export const sdSlice = createSlice({
|
|||||||
resetSeed: (state) => {
|
resetSeed: (state) => {
|
||||||
state.seed = -1;
|
state.seed = -1;
|
||||||
},
|
},
|
||||||
randomizeSeed: (state) => {
|
|
||||||
state.seed = randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX);
|
|
||||||
},
|
|
||||||
setParameter: (
|
setParameter: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{ key: string; value: string | number | boolean }>
|
action: PayloadAction<{ key: string; value: string | number | boolean }>
|
||||||
) => {
|
) => {
|
||||||
|
// TODO: This probably needs to be refactored.
|
||||||
const { key, value } = action.payload;
|
const { key, value } = action.payload;
|
||||||
const temp = { ...state, [key]: value };
|
const temp = { ...state, [key]: value };
|
||||||
if (key === 'seed') {
|
if (key === 'seed') {
|
||||||
@ -181,13 +147,14 @@ export const sdSlice = createSlice({
|
|||||||
setShouldGenerateVariations: (state, action: PayloadAction<boolean>) => {
|
setShouldGenerateVariations: (state, action: PayloadAction<boolean>) => {
|
||||||
state.shouldGenerateVariations = action.payload;
|
state.shouldGenerateVariations = action.payload;
|
||||||
},
|
},
|
||||||
setVariantAmount: (state, action: PayloadAction<number>) => {
|
setVariationAmount: (state, action: PayloadAction<number>) => {
|
||||||
state.variantAmount = action.payload;
|
state.variationAmount = action.payload;
|
||||||
},
|
},
|
||||||
setSeedWeights: (state, action: PayloadAction<string>) => {
|
setSeedWeights: (state, action: PayloadAction<string>) => {
|
||||||
state.seedWeights = action.payload;
|
state.seedWeights = action.payload;
|
||||||
},
|
},
|
||||||
setAllParameters: (state, action: PayloadAction<SDMetadata>) => {
|
setAllParameters: (state, action: PayloadAction<SDMetadata>) => {
|
||||||
|
// TODO: This probably needs to be refactored.
|
||||||
const {
|
const {
|
||||||
prompt,
|
prompt,
|
||||||
steps,
|
steps,
|
||||||
@ -283,13 +250,12 @@ export const {
|
|||||||
setInitialImagePath,
|
setInitialImagePath,
|
||||||
setMaskPath,
|
setMaskPath,
|
||||||
resetSeed,
|
resetSeed,
|
||||||
randomizeSeed,
|
|
||||||
resetSDState,
|
resetSDState,
|
||||||
setShouldFitToWidthHeight,
|
setShouldFitToWidthHeight,
|
||||||
setParameter,
|
setParameter,
|
||||||
setShouldGenerateVariations,
|
setShouldGenerateVariations,
|
||||||
setSeedWeights,
|
setSeedWeights,
|
||||||
setVariantAmount,
|
setVariationAmount,
|
||||||
setAllParameters,
|
setAllParameters,
|
||||||
setShouldRunGFPGAN,
|
setShouldRunGFPGAN,
|
||||||
setShouldRunESRGAN,
|
setShouldRunESRGAN,
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
IconButton,
|
IconButton,
|
||||||
useColorModeValue,
|
useColorModeValue,
|
||||||
Flex,
|
Flex,
|
||||||
Text,
|
Text,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from '../../app/store';
|
||||||
import { setShouldShowLogViewer, SystemState } from './systemSlice';
|
import { setShouldShowLogViewer, SystemState } from './systemSlice';
|
||||||
import { useLayoutEffect, useRef, useState } from 'react';
|
import { useLayoutEffect, useRef, useState } from 'react';
|
||||||
@ -14,112 +14,138 @@ import { createSelector } from '@reduxjs/toolkit';
|
|||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
|
|
||||||
const logSelector = createSelector(
|
const logSelector = createSelector(
|
||||||
(state: RootState) => state.system,
|
(state: RootState) => state.system,
|
||||||
(system: SystemState) => system.log,
|
(system: SystemState) => system.log,
|
||||||
{
|
{
|
||||||
memoizeOptions: {
|
memoizeOptions: {
|
||||||
resultEqualityCheck: (a, b) => a.length === b.length,
|
// We don't need a deep equality check for this selector.
|
||||||
},
|
resultEqualityCheck: (a, b) => a.length === b.length,
|
||||||
}
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const systemSelector = createSelector(
|
const systemSelector = createSelector(
|
||||||
(state: RootState) => state.system,
|
(state: RootState) => state.system,
|
||||||
(system: SystemState) => {
|
(system: SystemState) => {
|
||||||
return { shouldShowLogViewer: system.shouldShowLogViewer };
|
return { shouldShowLogViewer: system.shouldShowLogViewer };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: isEqual,
|
||||||
},
|
},
|
||||||
{
|
}
|
||||||
memoizeOptions: {
|
|
||||||
resultEqualityCheck: isEqual,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic log viewer, floats on bottom of page.
|
||||||
|
*/
|
||||||
const LogViewer = () => {
|
const LogViewer = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const bg = useColorModeValue('gray.50', 'gray.900');
|
const log = useAppSelector(logSelector);
|
||||||
const borderColor = useColorModeValue('gray.500', 'gray.500');
|
const { shouldShowLogViewer } = useAppSelector(systemSelector);
|
||||||
const [shouldAutoscroll, setShouldAutoscroll] = useState<boolean>(true);
|
|
||||||
|
|
||||||
const log = useAppSelector(logSelector);
|
// Set colors based on dark/light mode
|
||||||
const { shouldShowLogViewer } = useAppSelector(systemSelector);
|
const bg = useColorModeValue('gray.50', 'gray.900');
|
||||||
|
const borderColor = useColorModeValue('gray.500', 'gray.500');
|
||||||
|
const logTextColors = useColorModeValue(
|
||||||
|
{
|
||||||
|
info: undefined,
|
||||||
|
warning: 'yellow.500',
|
||||||
|
error: 'red.500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
info: undefined,
|
||||||
|
warning: 'yellow.300',
|
||||||
|
error: 'red.300',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const viewerRef = useRef<HTMLDivElement>(null);
|
// Rudimentary autoscroll
|
||||||
|
const [shouldAutoscroll, setShouldAutoscroll] = useState<boolean>(true);
|
||||||
|
const viewerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
/**
|
||||||
if (viewerRef.current !== null && shouldAutoscroll) {
|
* If autoscroll is on, scroll to the bottom when:
|
||||||
viewerRef.current.scrollTop = viewerRef.current.scrollHeight;
|
* - log updates
|
||||||
}
|
* - viewer is toggled
|
||||||
});
|
*
|
||||||
|
* Also scroll to the bottom whenever autoscroll is turned on.
|
||||||
|
*/
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (viewerRef.current !== null && shouldAutoscroll) {
|
||||||
|
viewerRef.current.scrollTop = viewerRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [shouldAutoscroll, log, shouldShowLogViewer]);
|
||||||
|
|
||||||
return (
|
const handleClickLogViewerToggle = () => {
|
||||||
<>
|
dispatch(setShouldShowLogViewer(!shouldShowLogViewer));
|
||||||
{shouldShowLogViewer && (
|
};
|
||||||
<Flex
|
|
||||||
position={'fixed'}
|
return (
|
||||||
left={0}
|
<>
|
||||||
bottom={0}
|
{shouldShowLogViewer && (
|
||||||
height='200px'
|
<Flex
|
||||||
width='100vw'
|
position={'fixed'}
|
||||||
overflow='auto'
|
left={0}
|
||||||
direction='column'
|
bottom={0}
|
||||||
fontFamily='monospace'
|
height="200px" // TODO: Make the log viewer resizeable.
|
||||||
fontSize='sm'
|
width="100vw"
|
||||||
pl={12}
|
overflow="auto"
|
||||||
pr={2}
|
direction="column"
|
||||||
pb={2}
|
fontFamily="monospace"
|
||||||
borderTopWidth='4px'
|
fontSize="sm"
|
||||||
borderColor={borderColor}
|
pl={12}
|
||||||
background={bg}
|
pr={2}
|
||||||
ref={viewerRef}
|
pb={2}
|
||||||
>
|
borderTopWidth="4px"
|
||||||
{log.map((entry, i) => (
|
borderColor={borderColor}
|
||||||
<Flex gap={2} key={i}>
|
background={bg}
|
||||||
<Text fontSize='sm' fontWeight={'semibold'}>
|
ref={viewerRef}
|
||||||
{entry.timestamp}:
|
>
|
||||||
</Text>
|
{log.map((entry, i) => {
|
||||||
<Text fontSize='sm' wordBreak={'break-all'}>
|
const { timestamp, message, level } = entry;
|
||||||
{entry.message}
|
return (
|
||||||
</Text>
|
<Flex gap={2} key={i} textColor={logTextColors[level]}>
|
||||||
</Flex>
|
<Text fontSize="sm" fontWeight={'semibold'}>
|
||||||
))}
|
{timestamp}:
|
||||||
</Flex>
|
</Text>
|
||||||
)}
|
<Text fontSize="sm" wordBreak={'break-all'}>
|
||||||
{shouldShowLogViewer && (
|
{message}
|
||||||
<Tooltip
|
</Text>
|
||||||
label={
|
</Flex>
|
||||||
shouldAutoscroll ? 'Autoscroll on' : 'Autoscroll off'
|
);
|
||||||
}
|
})}
|
||||||
>
|
</Flex>
|
||||||
<IconButton
|
)}
|
||||||
size='sm'
|
{shouldShowLogViewer && (
|
||||||
position={'fixed'}
|
<Tooltip label={shouldAutoscroll ? 'Autoscroll on' : 'Autoscroll off'}>
|
||||||
left={2}
|
<IconButton
|
||||||
bottom={12}
|
size="sm"
|
||||||
aria-label='Toggle autoscroll'
|
position={'fixed'}
|
||||||
variant={'solid'}
|
left={2}
|
||||||
colorScheme={shouldAutoscroll ? 'blue' : 'gray'}
|
bottom={12}
|
||||||
icon={<FaAngleDoubleDown />}
|
aria-label="Toggle autoscroll"
|
||||||
onClick={() => setShouldAutoscroll(!shouldAutoscroll)}
|
variant={'solid'}
|
||||||
/>
|
colorScheme={shouldAutoscroll ? 'blue' : 'gray'}
|
||||||
</Tooltip>
|
icon={<FaAngleDoubleDown />}
|
||||||
)}
|
onClick={() => setShouldAutoscroll(!shouldAutoscroll)}
|
||||||
<Tooltip label={shouldShowLogViewer ? 'Hide logs' : 'Show logs'}>
|
/>
|
||||||
<IconButton
|
</Tooltip>
|
||||||
size='sm'
|
)}
|
||||||
position={'fixed'}
|
<Tooltip label={shouldShowLogViewer ? 'Hide logs' : 'Show logs'}>
|
||||||
left={2}
|
<IconButton
|
||||||
bottom={2}
|
size="sm"
|
||||||
variant={'solid'}
|
position={'fixed'}
|
||||||
aria-label='Toggle Log Viewer'
|
left={2}
|
||||||
icon={shouldShowLogViewer ? <FaMinus /> : <FaCode />}
|
bottom={2}
|
||||||
onClick={() =>
|
variant={'solid'}
|
||||||
dispatch(setShouldShowLogViewer(!shouldShowLogViewer))
|
aria-label="Toggle Log Viewer"
|
||||||
}
|
icon={shouldShowLogViewer ? <FaMinus /> : <FaCode />}
|
||||||
/>
|
onClick={handleClickLogViewerToggle}
|
||||||
</Tooltip>
|
/>
|
||||||
</>
|
</Tooltip>
|
||||||
);
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LogViewer;
|
export default LogViewer;
|
||||||
|
@ -1,170 +1,164 @@
|
|||||||
import {
|
import {
|
||||||
Flex,
|
Button,
|
||||||
FormControl,
|
Flex,
|
||||||
FormLabel,
|
FormControl,
|
||||||
Heading,
|
FormLabel,
|
||||||
HStack,
|
Heading,
|
||||||
Modal,
|
HStack,
|
||||||
ModalBody,
|
Modal,
|
||||||
ModalCloseButton,
|
ModalBody,
|
||||||
ModalContent,
|
ModalCloseButton,
|
||||||
ModalFooter,
|
ModalContent,
|
||||||
ModalHeader,
|
ModalFooter,
|
||||||
ModalOverlay,
|
ModalHeader,
|
||||||
Switch,
|
ModalOverlay,
|
||||||
Text,
|
Switch,
|
||||||
useDisclosure,
|
Text,
|
||||||
|
useDisclosure,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
import {
|
import {
|
||||||
setShouldConfirmOnDelete,
|
setShouldConfirmOnDelete,
|
||||||
setShouldDisplayInProgress,
|
setShouldDisplayInProgress,
|
||||||
SystemState,
|
SystemState,
|
||||||
} from './systemSlice';
|
} from './systemSlice';
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from '../../app/store';
|
||||||
import SDButton from '../../components/SDButton';
|
|
||||||
import { persistor } from '../../main';
|
import { persistor } from '../../main';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { cloneElement, ReactElement } from 'react';
|
import { cloneElement, ReactElement } from 'react';
|
||||||
|
|
||||||
const systemSelector = createSelector(
|
const systemSelector = createSelector(
|
||||||
(state: RootState) => state.system,
|
(state: RootState) => state.system,
|
||||||
(system: SystemState) => {
|
(system: SystemState) => {
|
||||||
const { shouldDisplayInProgress, shouldConfirmOnDelete } = system;
|
const { shouldDisplayInProgress, shouldConfirmOnDelete } = system;
|
||||||
return { shouldDisplayInProgress, shouldConfirmOnDelete };
|
return { shouldDisplayInProgress, shouldConfirmOnDelete };
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
memoizeOptions: { resultEqualityCheck: isEqual },
|
memoizeOptions: { resultEqualityCheck: isEqual },
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
type Props = {
|
type SettingsModalProps = {
|
||||||
children: ReactElement;
|
/* The button to open the Settings Modal */
|
||||||
|
children: ReactElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SettingsModal = ({ children }: Props) => {
|
/**
|
||||||
const {
|
* Modal for app settings. Also provides Reset functionality in which the
|
||||||
isOpen: isSettingsModalOpen,
|
* app's localstorage is wiped via redux-persist.
|
||||||
onOpen: onSettingsModalOpen,
|
*
|
||||||
onClose: onSettingsModalClose,
|
* Secondary post-reset modal is included here.
|
||||||
} = useDisclosure();
|
*/
|
||||||
|
const SettingsModal = ({ children }: SettingsModalProps) => {
|
||||||
|
const {
|
||||||
|
isOpen: isSettingsModalOpen,
|
||||||
|
onOpen: onSettingsModalOpen,
|
||||||
|
onClose: onSettingsModalClose,
|
||||||
|
} = useDisclosure();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isOpen: isRefreshModalOpen,
|
isOpen: isRefreshModalOpen,
|
||||||
onOpen: onRefreshModalOpen,
|
onOpen: onRefreshModalOpen,
|
||||||
onClose: onRefreshModalClose,
|
onClose: onRefreshModalClose,
|
||||||
} = useDisclosure();
|
} = useDisclosure();
|
||||||
|
|
||||||
const { shouldDisplayInProgress, shouldConfirmOnDelete } =
|
const { shouldDisplayInProgress, shouldConfirmOnDelete } =
|
||||||
useAppSelector(systemSelector);
|
useAppSelector(systemSelector);
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const handleClickResetWebUI = () => {
|
/**
|
||||||
persistor.purge().then(() => {
|
* Resets localstorage, then opens a secondary modal informing user to
|
||||||
onSettingsModalClose();
|
* refresh their browser.
|
||||||
onRefreshModalOpen();
|
* */
|
||||||
});
|
const handleClickResetWebUI = () => {
|
||||||
};
|
persistor.purge().then(() => {
|
||||||
|
onSettingsModalClose();
|
||||||
|
onRefreshModalOpen();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{cloneElement(children, {
|
{cloneElement(children, {
|
||||||
onClick: onSettingsModalOpen,
|
onClick: onSettingsModalOpen,
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<Modal isOpen={isSettingsModalOpen} onClose={onSettingsModalClose}>
|
<Modal isOpen={isSettingsModalOpen} onClose={onSettingsModalClose}>
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>Settings</ModalHeader>
|
<ModalHeader>Settings</ModalHeader>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Flex gap={5} direction='column'>
|
<Flex gap={5} direction="column">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<HStack>
|
<HStack>
|
||||||
<FormLabel marginBottom={1}>
|
<FormLabel marginBottom={1}>
|
||||||
Display in-progress images (slower)
|
Display in-progress images (slower)
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Switch
|
<Switch
|
||||||
isChecked={shouldDisplayInProgress}
|
isChecked={shouldDisplayInProgress}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
dispatch(
|
dispatch(setShouldDisplayInProgress(e.target.checked))
|
||||||
setShouldDisplayInProgress(
|
}
|
||||||
e.target.checked
|
/>
|
||||||
)
|
</HStack>
|
||||||
)
|
</FormControl>
|
||||||
}
|
<FormControl>
|
||||||
/>
|
<HStack>
|
||||||
</HStack>
|
<FormLabel marginBottom={1}>Confirm on delete</FormLabel>
|
||||||
</FormControl>
|
<Switch
|
||||||
<FormControl>
|
isChecked={shouldConfirmOnDelete}
|
||||||
<HStack>
|
onChange={(e) =>
|
||||||
<FormLabel marginBottom={1}>
|
dispatch(setShouldConfirmOnDelete(e.target.checked))
|
||||||
Confirm on delete
|
}
|
||||||
</FormLabel>
|
/>
|
||||||
<Switch
|
</HStack>
|
||||||
isChecked={shouldConfirmOnDelete}
|
</FormControl>
|
||||||
onChange={(e) =>
|
|
||||||
dispatch(
|
|
||||||
setShouldConfirmOnDelete(
|
|
||||||
e.target.checked
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<Heading size={'md'}>Reset Web UI</Heading>
|
<Heading size={'md'}>Reset Web UI</Heading>
|
||||||
<Text>
|
<Text>
|
||||||
Resetting the web UI only resets the browser's
|
Resetting the web UI only resets the browser's local cache of
|
||||||
local cache of your images and remembered
|
your images and remembered settings. It does not delete any
|
||||||
settings. It does not delete any images from
|
images from disk.
|
||||||
disk.
|
</Text>
|
||||||
</Text>
|
<Text>
|
||||||
<Text>
|
If images aren't showing up in the gallery or something else
|
||||||
If images aren't showing up in the gallery or
|
isn't working, please try resetting before submitting an issue
|
||||||
something else isn't working, please try
|
on GitHub.
|
||||||
resetting before submitting an issue on GitHub.
|
</Text>
|
||||||
</Text>
|
<Button colorScheme="red" onClick={handleClickResetWebUI}>
|
||||||
<SDButton
|
Reset Web UI
|
||||||
label='Reset Web UI'
|
</Button>
|
||||||
colorScheme='red'
|
</Flex>
|
||||||
onClick={handleClickResetWebUI}
|
</ModalBody>
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<SDButton
|
<Button onClick={onSettingsModalClose}>Close</Button>
|
||||||
label='Close'
|
</ModalFooter>
|
||||||
onClick={onSettingsModalClose}
|
</ModalContent>
|
||||||
/>
|
</Modal>
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
closeOnOverlayClick={false}
|
closeOnOverlayClick={false}
|
||||||
isOpen={isRefreshModalOpen}
|
isOpen={isRefreshModalOpen}
|
||||||
onClose={onRefreshModalClose}
|
onClose={onRefreshModalClose}
|
||||||
isCentered
|
isCentered
|
||||||
>
|
>
|
||||||
<ModalOverlay bg='blackAlpha.300' backdropFilter='blur(40px)' />
|
<ModalOverlay bg="blackAlpha.300" backdropFilter="blur(40px)" />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalBody pb={6} pt={6}>
|
<ModalBody pb={6} pt={6}>
|
||||||
<Flex justifyContent={'center'}>
|
<Flex justifyContent={'center'}>
|
||||||
<Text fontSize={'lg'}>
|
<Text fontSize={'lg'}>
|
||||||
Web UI has been reset. Refresh the page to
|
Web UI has been reset. Refresh the page to reload.
|
||||||
reload.
|
</Text>
|
||||||
</Text>
|
</Flex>
|
||||||
</Flex>
|
</ModalBody>
|
||||||
</ModalBody>
|
</ModalContent>
|
||||||
</ModalContent>
|
</Modal>
|
||||||
</Modal>
|
</>
|
||||||
</>
|
);
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SettingsModal;
|
export default SettingsModal;
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import dateFormat from 'dateformat';
|
|
||||||
import { ExpandedIndex } from '@chakra-ui/react';
|
import { ExpandedIndex } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
export type LogLevel = 'info' | 'warning' | 'error';
|
||||||
|
|
||||||
export interface LogEntry {
|
export interface LogEntry {
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
|
level: LogLevel;
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -12,10 +14,18 @@ export interface Log {
|
|||||||
[index: number]: LogEntry;
|
[index: number]: LogEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SystemState {
|
export interface SystemStatus {
|
||||||
shouldDisplayInProgress: boolean;
|
|
||||||
isProcessing: boolean;
|
isProcessing: boolean;
|
||||||
currentStep: number;
|
currentStep: number;
|
||||||
|
totalSteps: number;
|
||||||
|
currentIteration: number;
|
||||||
|
totalIterations: number;
|
||||||
|
currentStatus: string;
|
||||||
|
currentStatusHasSteps: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemState extends SystemStatus {
|
||||||
|
shouldDisplayInProgress: boolean;
|
||||||
log: Array<LogEntry>;
|
log: Array<LogEntry>;
|
||||||
shouldShowLogViewer: boolean;
|
shouldShowLogViewer: boolean;
|
||||||
isGFPGANAvailable: boolean;
|
isGFPGANAvailable: boolean;
|
||||||
@ -24,12 +34,17 @@ export interface SystemState {
|
|||||||
socketId: string;
|
socketId: string;
|
||||||
shouldConfirmOnDelete: boolean;
|
shouldConfirmOnDelete: boolean;
|
||||||
openAccordions: ExpandedIndex;
|
openAccordions: ExpandedIndex;
|
||||||
|
currentStep: number;
|
||||||
|
totalSteps: number;
|
||||||
|
currentIteration: number;
|
||||||
|
totalIterations: number;
|
||||||
|
currentStatus: string;
|
||||||
|
currentStatusHasSteps: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialSystemState = {
|
const initialSystemState = {
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
isProcessing: false,
|
isProcessing: false,
|
||||||
currentStep: 0,
|
|
||||||
log: [],
|
log: [],
|
||||||
shouldShowLogViewer: false,
|
shouldShowLogViewer: false,
|
||||||
shouldDisplayInProgress: false,
|
shouldDisplayInProgress: false,
|
||||||
@ -38,6 +53,12 @@ const initialSystemState = {
|
|||||||
socketId: '',
|
socketId: '',
|
||||||
shouldConfirmOnDelete: true,
|
shouldConfirmOnDelete: true,
|
||||||
openAccordions: [0],
|
openAccordions: [0],
|
||||||
|
currentStep: 0,
|
||||||
|
totalSteps: 0,
|
||||||
|
currentIteration: 0,
|
||||||
|
totalIterations: 0,
|
||||||
|
currentStatus: '',
|
||||||
|
currentStatusHasSteps: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState: SystemState = initialSystemState;
|
const initialState: SystemState = initialSystemState;
|
||||||
@ -51,18 +72,35 @@ export const systemSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setIsProcessing: (state, action: PayloadAction<boolean>) => {
|
setIsProcessing: (state, action: PayloadAction<boolean>) => {
|
||||||
state.isProcessing = action.payload;
|
state.isProcessing = action.payload;
|
||||||
if (action.payload === false) {
|
|
||||||
state.currentStep = 0;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
setCurrentStep: (state, action: PayloadAction<number>) => {
|
setCurrentStatus: (state, action: PayloadAction<string>) => {
|
||||||
state.currentStep = action.payload;
|
state.currentStatus = action.payload;
|
||||||
},
|
},
|
||||||
addLogEntry: (state, action: PayloadAction<string>) => {
|
setSystemStatus: (state, action: PayloadAction<SystemStatus>) => {
|
||||||
|
const currentStatus =
|
||||||
|
!action.payload.isProcessing && state.isConnected
|
||||||
|
? 'Connected'
|
||||||
|
: action.payload.currentStatus;
|
||||||
|
|
||||||
|
return { ...state, ...action.payload, currentStatus };
|
||||||
|
},
|
||||||
|
addLogEntry: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
timestamp: string;
|
||||||
|
message: string;
|
||||||
|
level?: LogLevel;
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
const { timestamp, message, level } = action.payload;
|
||||||
|
const logLevel = level || 'info';
|
||||||
|
|
||||||
const entry: LogEntry = {
|
const entry: LogEntry = {
|
||||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
timestamp,
|
||||||
message: action.payload,
|
message,
|
||||||
|
level: logLevel,
|
||||||
};
|
};
|
||||||
|
|
||||||
state.log.push(entry);
|
state.log.push(entry);
|
||||||
},
|
},
|
||||||
setShouldShowLogViewer: (state, action: PayloadAction<boolean>) => {
|
setShouldShowLogViewer: (state, action: PayloadAction<boolean>) => {
|
||||||
@ -86,13 +124,14 @@ export const systemSlice = createSlice({
|
|||||||
export const {
|
export const {
|
||||||
setShouldDisplayInProgress,
|
setShouldDisplayInProgress,
|
||||||
setIsProcessing,
|
setIsProcessing,
|
||||||
setCurrentStep,
|
|
||||||
addLogEntry,
|
addLogEntry,
|
||||||
setShouldShowLogViewer,
|
setShouldShowLogViewer,
|
||||||
setIsConnected,
|
setIsConnected,
|
||||||
setSocketId,
|
setSocketId,
|
||||||
setShouldConfirmOnDelete,
|
setShouldConfirmOnDelete,
|
||||||
setOpenAccordions,
|
setOpenAccordions,
|
||||||
|
setSystemStatus,
|
||||||
|
setCurrentStatus,
|
||||||
} = systemSlice.actions;
|
} = systemSlice.actions;
|
||||||
|
|
||||||
export default systemSlice.reducer;
|
export default systemSlice.reducer;
|
||||||
|
@ -1,108 +0,0 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { isEqual } from 'lodash';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { useAppSelector } from '../../app/hooks';
|
|
||||||
import { RootState } from '../../app/store';
|
|
||||||
import { SDState } from '../sd/sdSlice';
|
|
||||||
import { validateSeedWeights } from '../sd/util/seedWeightPairs';
|
|
||||||
import { SystemState } from './systemSlice';
|
|
||||||
|
|
||||||
const sdSelector = createSelector(
|
|
||||||
(state: RootState) => state.sd,
|
|
||||||
(sd: SDState) => {
|
|
||||||
return {
|
|
||||||
prompt: sd.prompt,
|
|
||||||
shouldGenerateVariations: sd.shouldGenerateVariations,
|
|
||||||
seedWeights: sd.seedWeights,
|
|
||||||
maskPath: sd.maskPath,
|
|
||||||
initialImagePath: sd.initialImagePath,
|
|
||||||
seed: sd.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.
|
|
||||||
|
|
||||||
Other parameter values may cause failure but we rely on input validation for those.
|
|
||||||
*/
|
|
||||||
const useCheckParameters = () => {
|
|
||||||
const {
|
|
||||||
prompt,
|
|
||||||
shouldGenerateVariations,
|
|
||||||
seedWeights,
|
|
||||||
maskPath,
|
|
||||||
initialImagePath,
|
|
||||||
seed,
|
|
||||||
} = useAppSelector(sdSelector);
|
|
||||||
|
|
||||||
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;
|
|
@ -8,9 +8,9 @@ import { persistStore } from 'redux-persist';
|
|||||||
|
|
||||||
export const persistor = persistStore(store);
|
export const persistor = persistStore(store);
|
||||||
|
|
||||||
import App from './App';
|
|
||||||
import { theme } from './app/theme';
|
import { theme } from './app/theme';
|
||||||
import Loading from './Loading';
|
import Loading from './Loading';
|
||||||
|
import App from './app/App';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
@ -34,6 +34,7 @@ class PngWriter:
|
|||||||
# saves image named _image_ to outdir/name, writing metadata from prompt
|
# saves image named _image_ to outdir/name, writing metadata from prompt
|
||||||
# returns full path of output
|
# returns full path of output
|
||||||
def save_image_and_prompt_to_png(self, image, dream_prompt, name, metadata=None):
|
def save_image_and_prompt_to_png(self, image, dream_prompt, name, metadata=None):
|
||||||
|
print(f'self.outdir={self.outdir}, name={name}')
|
||||||
path = os.path.join(self.outdir, name)
|
path = os.path.join(self.outdir, name)
|
||||||
info = PngImagePlugin.PngInfo()
|
info = PngImagePlugin.PngInfo()
|
||||||
info.add_text('Dream', dream_prompt)
|
info.add_text('Dream', dream_prompt)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user