Merge pull request #1132 from cmdr2/beta

Beta
This commit is contained in:
cmdr2 2023-04-07 15:08:25 +05:30 committed by GitHub
commit 7a2048b2cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 478 additions and 76 deletions

View File

@ -21,6 +21,9 @@
Our focus continues to remain on an easy installation experience, and an easy user-interface. While still remaining pretty powerful, in terms of features and speed. Our focus continues to remain on an easy installation experience, and an easy user-interface. While still remaining pretty powerful, in terms of features and speed.
### Detailed changelog ### Detailed changelog
* 2.5.31 - 6 Apr 2023 - Allow seeds upto `4,294,967,295`. Thanks @ogmaresca.
* 2.5.31 - 6 Apr 2023 - Buttons to show the previous/next image in the image popup. Thanks @ogmaresca.
* 2.5.30 - 5 Apr 2023 - Fix a bug where the JPEG image quality wasn't being respected when embedding the metadata into it. Thanks @JeLuf.
* 2.5.30 - 1 Apr 2023 - (beta-only) Slider to control the strength of the LoRA model. * 2.5.30 - 1 Apr 2023 - (beta-only) Slider to control the strength of the LoRA model.
* 2.5.30 - 28 Mar 2023 - Refactor task entry config to use a generating method. Added ability for plugins to easily add to this. Removed confusing sentence from `contributing.md` * 2.5.30 - 28 Mar 2023 - Refactor task entry config to use a generating method. Added ability for plugins to easily add to this. Removed confusing sentence from `contributing.md`
* 2.5.30 - 28 Mar 2023 - Allow the user to undo the deletion of tasks or images, instead of showing a pop-up each time. The new `Undo` button will be present at the top of the UI. Thanks @JeLuf. * 2.5.30 - 28 Mar 2023 - Allow the user to undo the deletion of tasks or images, instead of showing a pop-up each time. The new `Undo` button will be present at the top of the UI. Thanks @JeLuf.

View File

@ -10,7 +10,7 @@ from typing import List, Union
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from starlette.responses import FileResponse, JSONResponse, StreamingResponse from starlette.responses import FileResponse, JSONResponse, StreamingResponse
from pydantic import BaseModel from pydantic import BaseModel, Extra
from easydiffusion import app, model_manager, task_manager from easydiffusion import app, model_manager, task_manager
from easydiffusion.types import TaskData, GenerateImageRequest, MergeRequest from easydiffusion.types import TaskData, GenerateImageRequest, MergeRequest
@ -44,7 +44,7 @@ class NoCacheStaticFiles(StaticFiles):
return super().is_not_modified(response_headers, request_headers) return super().is_not_modified(response_headers, request_headers)
class SetAppConfigRequest(BaseModel): class SetAppConfigRequest(BaseModel, extra=Extra.allow):
update_branch: str = None update_branch: str = None
render_devices: Union[List[str], List[int], str, int] = None render_devices: Union[List[str], List[int], str, int] = None
model_vae: str = None model_vae: str = None
@ -136,6 +136,10 @@ def set_app_config_internal(req: SetAppConfigRequest):
config["test_diffusers"] = req.test_diffusers config["test_diffusers"] = req.test_diffusers
for property, property_value in req.dict().items():
if property_value is not None and property not in req.__fields__:
config[property] = property_value
try: try:
app.setConfig(config) app.setConfig(config)

View File

@ -1,13 +1,18 @@
import os import os
import time import time
import base64
import re import re
from easydiffusion import app
from easydiffusion.types import TaskData, GenerateImageRequest from easydiffusion.types import TaskData, GenerateImageRequest
from functools import reduce
from datetime import datetime
from sdkit.utils import save_images, save_dicts from sdkit.utils import save_images, save_dicts
from numpy import base_repr from numpy import base_repr
filename_regex = re.compile("[^a-zA-Z0-9._-]") filename_regex = re.compile("[^a-zA-Z0-9._-]")
img_number_regex = re.compile("([0-9]{5,})")
# keep in sync with `ui/media/js/dnd.js` # keep in sync with `ui/media/js/dnd.js`
TASK_TEXT_MAPPING = { TASK_TEXT_MAPPING = {
@ -28,15 +33,89 @@ TASK_TEXT_MAPPING = {
"use_hypernetwork_model": "Hypernetwork model", "use_hypernetwork_model": "Hypernetwork model",
"hypernetwork_strength": "Hypernetwork Strength", "hypernetwork_strength": "Hypernetwork Strength",
"use_lora_model": "LoRA model", "use_lora_model": "LoRA model",
# "lora_alpha": "LoRA Strength", "lora_alpha": "LoRA Strength",
} }
time_placeholders = {
"$yyyy": "%Y",
"$MM": "%m",
"$dd": "%d",
"$HH": "%H",
"$mm": "%M",
"$ss": "%S",
}
other_placeholders = {
"$id": lambda req, task_data: filename_regex.sub("_", task_data.session_id),
"$p": lambda req, task_data: filename_regex.sub("_", req.prompt)[:50],
"$s": lambda req, task_data: str(req.seed),
}
class ImageNumber:
_factory = None
_evaluated = False
def __init__(self, factory):
self._factory = factory
self._evaluated = None
def __call__(self) -> int:
if self._evaluated is None:
self._evaluated = self._factory()
return self._evaluated
def format_placeholders(format: str, req: GenerateImageRequest, task_data: TaskData, now = None):
if now is None:
now = time.time()
for placeholder, time_format in time_placeholders.items():
if placeholder in format:
format = format.replace(placeholder, datetime.fromtimestamp(now).strftime(time_format))
for placeholder, replace_func in other_placeholders.items():
if placeholder in format:
format = format.replace(placeholder, replace_func(req, task_data))
return format
def format_folder_name(format: str, req: GenerateImageRequest, task_data: TaskData):
format = format_placeholders(format, req, task_data)
return filename_regex.sub("_", format)
def format_file_name(
format: str,
req: GenerateImageRequest,
task_data: TaskData,
now: float,
batch_file_number: int,
folder_img_number: ImageNumber,
):
format = format_placeholders(format, req, task_data, now)
if "$n" in format:
format = format.replace("$n", f"{folder_img_number():05}")
if "$tsb64" in format:
img_id = base_repr(int(now * 10000), 36)[-7:] + base_repr(int(batch_file_number), 36) # Base 36 conversion, 0-9, A-Z
format = format.replace("$tsb64", img_id)
if "$ts" in format:
format = format.replace("$ts", str(int(now * 1000) + batch_file_number))
return filename_regex.sub("_", format)
def save_images_to_disk(images: list, filtered_images: list, req: GenerateImageRequest, task_data: TaskData): def save_images_to_disk(images: list, filtered_images: list, req: GenerateImageRequest, task_data: TaskData):
now = time.time() now = time.time()
save_dir_path = os.path.join(task_data.save_to_disk_path, filename_regex.sub("_", task_data.session_id)) app_config = app.getConfig()
folder_format = app_config.get("folder_format", "$id")
save_dir_path = os.path.join(task_data.save_to_disk_path, format_folder_name(folder_format, req, task_data))
metadata_entries = get_metadata_entries_for_request(req, task_data) metadata_entries = get_metadata_entries_for_request(req, task_data)
make_filename = make_filename_callback(req, now=now) file_number = calculate_img_number(save_dir_path, task_data)
make_filename = make_filename_callback(
app_config.get("filename_format", "$p_$tsb64"),
req,
task_data,
file_number,
now=now,
)
if task_data.show_only_filtered_image or filtered_images is images: if task_data.show_only_filtered_image or filtered_images is images:
save_images( save_images(
@ -58,7 +137,7 @@ def save_images_to_disk(images: list, filtered_images: list, req: GenerateImageR
file_format=task_data.output_format, file_format=task_data.output_format,
) )
else: else:
make_filter_filename = make_filename_callback(req, now=now, suffix="filtered") make_filter_filename = make_filename_callback(req, task_data, file_number, now=now, suffix="filtered")
save_images( save_images(
images, images,
@ -105,9 +184,6 @@ def get_metadata_entries_for_request(req: GenerateImageRequest, task_data: TaskD
if task_data.use_lora_model is None: if task_data.use_lora_model is None:
if "lora_alpha" in metadata: if "lora_alpha" in metadata:
del metadata["lora_alpha"] del metadata["lora_alpha"]
from easydiffusion import app
app_config = app.getConfig() app_config = app.getConfig()
if not app_config.get("test_diffusers", False) and "use_lora_model" in metadata: if not app_config.get("test_diffusers", False) and "use_lora_model" in metadata:
del metadata["use_lora_model"] del metadata["use_lora_model"]
@ -133,16 +209,66 @@ def get_printable_request(req: GenerateImageRequest):
return metadata return metadata
def make_filename_callback(req: GenerateImageRequest, suffix=None, now=None): def make_filename_callback(
filename_format: str,
req: GenerateImageRequest,
task_data: TaskData,
folder_img_number: int,
suffix=None,
now=None,
):
if now is None: if now is None:
now = time.time() now = time.time()
def make_filename(i): def make_filename(i):
img_id = base_repr(int(now * 10000), 36)[-7:] + base_repr(int(i),36) # Base 36 conversion, 0-9, A-Z name = format_file_name(filename_format, req, task_data, now, i, folder_img_number)
prompt_flattened = filename_regex.sub("_", req.prompt)[:50]
name = f"{prompt_flattened}_{img_id}"
name = name if suffix is None else f"{name}_{suffix}" name = name if suffix is None else f"{name}_{suffix}"
return name return name
return make_filename return make_filename
def _calculate_img_number(save_dir_path: str, task_data: TaskData):
def get_highest_img_number(accumulator: int, file: os.DirEntry) -> int:
if not file.is_file:
return accumulator
if len(list(filter(lambda e: file.name.endswith(e), app.IMAGE_EXTENSIONS))) == 0:
return accumulator
get_highest_img_number.number_of_images = get_highest_img_number.number_of_images + 1
number_match = img_number_regex.match(file.name)
if not number_match:
return accumulator
file_number = number_match.group().lstrip('0')
# Handle 00000
return int(file_number) if file_number else 0
get_highest_img_number.number_of_images = 0
highest_file_number = -1
if os.path.isdir(save_dir_path):
existing_files = list(os.scandir(save_dir_path))
highest_file_number = reduce(get_highest_img_number, existing_files, -1)
calculated_img_number = max(highest_file_number, get_highest_img_number.number_of_images - 1)
if task_data.session_id in _calculate_img_number.session_img_numbers:
calculated_img_number = max(
_calculate_img_number.session_img_numbers[task_data.session_id],
calculated_img_number,
)
calculated_img_number = calculated_img_number + 1
_calculate_img_number.session_img_numbers[task_data.session_id] = calculated_img_number
return calculated_img_number
_calculate_img_number.session_img_numbers = {}
def calculate_img_number(save_dir_path: str, task_data: TaskData):
return ImageNumber(lambda: _calculate_img_number(save_dir_path, task_data))

View File

@ -30,7 +30,7 @@
<h1> <h1>
<img id="logo_img" src="/media/images/icon-512x512.png" > <img id="logo_img" src="/media/images/icon-512x512.png" >
Easy Diffusion Easy Diffusion
<small>v2.5.30 <span id="updateBranchLabel"></span></small> <small>v2.5.31 <span id="updateBranchLabel"></span></small>
</h1> </h1>
</div> </div>
<div id="server-status"> <div id="server-status">
@ -221,7 +221,7 @@
<input id="lora_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" /> <input id="lora_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
</td></tr> </td></tr>
<tr id="lora_alpha_container" class="pl-5"> <tr id="lora_alpha_container" class="pl-5">
<td><label for="lora_alpha_slider">LoRA strength:</label></td> <td><label for="lora_alpha_slider">LoRA Strength:</label></td>
<td> <input id="lora_alpha_slider" name="lora_alpha_slider" class="editor-slider" value="50" type="range" min="0" max="100"> <input id="lora_alpha" name="lora_alpha" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)"><br/></td> <td> <input id="lora_alpha_slider" name="lora_alpha_slider" class="editor-slider" value="50" type="range" min="0" max="100"> <input id="lora_alpha" name="lora_alpha" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)"><br/></td>
</tr> </tr>
<tr class="pl-5"><td><label for="hypernetwork_model">Hypernetwork:</i></label></td><td> <tr class="pl-5"><td><label for="hypernetwork_model">Hypernetwork:</i></label></td><td>
@ -293,6 +293,12 @@
<div id="preview" class="col-free"> <div id="preview" class="col-free">
<div id="initial-text">
Type a prompt and press the "Make Image" button.<br/><br/>You can set an "Initial Image" if you want to guide the AI.<br/><br/>
You can also add modifiers like "Realistic", "Pencil Sketch", "ArtStation" etc by browsing through the "Image Modifiers" section
and selecting the desired modifiers.<br/><br/>
Click "Image Settings" for additional settings like seed, image size, number of images to generate etc.<br/><br/>Enjoy! :)
</div>
<div id="preview-content"> <div id="preview-content">
<div id="preview-tools" class="displayNone"> <div id="preview-tools" class="displayNone">
<button id="clear-all-previews" class="secondaryButton"><i class="fa-solid fa-trash-can icon"></i> Clear All</button> <button id="clear-all-previews" class="secondaryButton"><i class="fa-solid fa-trash-can icon"></i> Clear All</button>
@ -326,12 +332,6 @@
<div class="clearfix" style="clear: both;"></div> <div class="clearfix" style="clear: both;"></div>
</div> </div>
</div> </div>
<div id="initial-text">
Type a prompt and press the "Make Image" button.<br/><br/>You can set an "Initial Image" if you want to guide the AI.<br/><br/>
You can also add modifiers like "Realistic", "Pencil Sketch", "ArtStation" etc by browsing through the "Image Modifiers" section
and selecting the desired modifiers.<br/><br/>
Click "Image Settings" for additional settings like seed, image size, number of images to generate etc.<br/><br/>Enjoy! :)
</div>
</div> </div>
</div> </div>

View File

@ -3,7 +3,7 @@
font-family: 'Work Sans'; font-family: 'Work Sans';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local(''), src: local('Work Sans'),
url('/media/fonts/work-sans-v18-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ url('/media/fonts/work-sans-v18-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('/media/fonts/work-sans-v18-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ url('/media/fonts/work-sans-v18-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
} }
@ -13,7 +13,7 @@
font-family: 'Work Sans'; font-family: 'Work Sans';
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
src: local(''), src: local('Work Sans'),
url('/media/fonts/work-sans-v18-latin-600.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ url('/media/fonts/work-sans-v18-latin-600.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('/media/fonts/work-sans-v18-latin-600.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ url('/media/fonts/work-sans-v18-latin-600.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
} }
@ -23,7 +23,7 @@
font-family: 'Work Sans'; font-family: 'Work Sans';
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: local(''), src: local('Work Sans'),
url('/media/fonts/work-sans-v18-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ url('/media/fonts/work-sans-v18-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('/media/fonts/work-sans-v18-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ url('/media/fonts/work-sans-v18-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
} }
@ -33,7 +33,7 @@
font-family: 'Work Sans'; font-family: 'Work Sans';
font-style: normal; font-style: normal;
font-weight: 800; font-weight: 800;
src: local(''), src: local('Work Sans'),
url('/media/fonts/work-sans-v18-latin-800.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ url('/media/fonts/work-sans-v18-latin-800.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('/media/fonts/work-sans-v18-latin-800.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ url('/media/fonts/work-sans-v18-latin-800.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
} }

View File

@ -238,6 +238,10 @@ code {
#stopImage:hover { #stopImage:hover {
background: rgb(177, 27, 0); background: rgb(177, 27, 0);
} }
#undo {
float: right;
margin-left: 5px;
}
div#render-buttons { div#render-buttons {
gap: 3px; gap: 3px;

View File

@ -245,6 +245,14 @@ const TASK_MAPPING = {
readUI: () => loraModelField.value, readUI: () => loraModelField.value,
parse: (val) => val parse: (val) => val
}, },
lora_alpha: { name: 'LoRA Strength',
setUI: (lora_alpha) => {
loraAlphaField.value = lora_alpha
updateLoraAlphaSlider()
},
readUI: () => parseFloat(loraAlphaField.value),
parse: (val) => parseFloat(val)
},
use_hypernetwork_model: { name: 'Hypernetwork model', use_hypernetwork_model: { name: 'Hypernetwork model',
setUI: (use_hypernetwork_model) => { setUI: (use_hypernetwork_model) => {
const oldVal = hypernetworkModelField.value const oldVal = hypernetworkModelField.value
@ -341,6 +349,11 @@ function restoreTaskToUI(task, fieldsToSkip) {
hypernetworkModelField.dispatchEvent(new Event("change")) hypernetworkModelField.dispatchEvent(new Event("change"))
} }
if (!('use_lora_model' in task.reqBody)) {
loraModelField.value = "None"
loraModelField.dispatchEvent(new Event("change"))
}
// restore the original prompt if provided (e.g. use settings), fallback to prompt as needed (e.g. copy/paste or d&d) // restore the original prompt if provided (e.g. use settings), fallback to prompt as needed (e.g. copy/paste or d&d)
promptField.value = task.reqBody.original_prompt promptField.value = task.reqBody.original_prompt
if (!('original_prompt' in task.reqBody)) { if (!('original_prompt' in task.reqBody)) {

View File

@ -1,6 +1,28 @@
"use strict" "use strict"
/**
* @typedef {object} ImageModalRequest
* @property {string} src
* @property {ImageModalRequest | () => ImageModalRequest | undefined} previous
* @property {ImageModalRequest | () => ImageModalRequest | undefined} next
*/
/**
* @type {(() => (string | ImageModalRequest) | string | ImageModalRequest) => {}}
*/
const imageModal = (function() { const imageModal = (function() {
const backElem = createElement(
'i',
undefined,
['fa-solid', 'fa-arrow-left', 'tertiaryButton'],
)
const forwardElem = createElement(
'i',
undefined,
['fa-solid', 'fa-arrow-right', 'tertiaryButton'],
)
const zoomElem = createElement( const zoomElem = createElement(
'i', 'i',
undefined, undefined,
@ -13,7 +35,7 @@ const imageModal = (function() {
['fa-solid', 'fa-xmark', 'tertiaryButton'], ['fa-solid', 'fa-xmark', 'tertiaryButton'],
) )
const menuBarElem = createElement('div', undefined, 'menu-bar', [zoomElem, closeElem]) const menuBarElem = createElement('div', undefined, 'menu-bar', [backElem, forwardElem, zoomElem, closeElem])
const imageContainer = createElement('div', undefined, 'image-wrapper') const imageContainer = createElement('div', undefined, 'image-wrapper')
@ -63,15 +85,87 @@ const imageModal = (function() {
() => setZoomLevel(imageContainer.querySelector('img')?.classList?.contains('natural-zoom')), () => setZoomLevel(imageContainer.querySelector('img')?.classList?.contains('natural-zoom')),
) )
const close = () => { const state = {
previous: undefined,
next: undefined,
}
const clear = () => {
imageContainer.innerHTML = '' imageContainer.innerHTML = ''
Object.keys(state).forEach(key => delete state[key])
}
const close = () => {
clear()
modalElem.classList.remove('active') modalElem.classList.remove('active')
document.body.style.overflow = 'initial' document.body.style.overflow = 'initial'
} }
window.addEventListener('keydown', (e) => { /**
if (e.key === 'Escape' && modalElem.classList.contains('active')) { * @param {() => (string | ImageModalRequest) | string | ImageModalRequest} optionsFactory
*/
function init(optionsFactory) {
if (!optionsFactory) {
close() close()
return
}
clear()
const options = typeof optionsFactory === 'function' ? optionsFactory() : optionsFactory
const src = typeof options === 'string' ? options : options.src
const imgElem = createElement('img', { src }, 'natural-zoom')
imageContainer.appendChild(imgElem)
modalElem.classList.add('active')
document.body.style.overflow = 'hidden'
setZoomLevel(false)
if (typeof options === 'object' && options.previous) {
state.previous = options.previous
backElem.style.display = 'unset'
} else {
backElem.style.display = 'none'
}
if (typeof options === 'object' && options.next) {
state.next = options.next
forwardElem.style.display = 'unset'
} else {
forwardElem.style.display = 'none'
}
}
const back = () => {
if (state.previous) {
init(state.previous)
} else {
backElem.style.display = 'none'
}
}
const forward = () => {
if (state.next) {
init(state.next)
} else {
forwardElem.style.display = 'none'
}
}
window.addEventListener('keydown', (e) => {
if (modalElem.classList.contains('active')) {
switch (e.key) {
case 'Escape':
close()
break
case 'ArrowLeft':
back()
break
case 'ArrowRight':
forward()
break
}
} }
}) })
window.addEventListener('click', (e) => { window.addEventListener('click', (e) => {
@ -86,15 +180,12 @@ const imageModal = (function() {
} }
}) })
return (optionsFactory) => { backElem.addEventListener('click', back)
const options = typeof optionsFactory === 'function' ? optionsFactory() : optionsFactory
const src = typeof options === 'string' ? options : options.src
// TODO center it if < window size forwardElem.addEventListener('click', forward)
const imgElem = createElement('img', { src }, 'natural-zoom')
imageContainer.appendChild(imgElem) /**
modalElem.classList.add('active') * @param {() => (string | ImageModalRequest) | string | ImageModalRequest} optionsFactory
document.body.style.overflow = 'hidden' */
setZoomLevel(false) return (optionsFactory) => init(optionsFactory)
}
})() })()

View File

@ -398,7 +398,30 @@ function showImages(reqBody, res, outputContainer, livePreview) {
if ('seed' in result && !imageElem.hasAttribute('data-seed')) { if ('seed' in result && !imageElem.hasAttribute('data-seed')) {
const imageExpandBtn = imageItemElem.querySelector('.imgExpandBtn') const imageExpandBtn = imageItemElem.querySelector('.imgExpandBtn')
imageExpandBtn.addEventListener('click', function() { imageExpandBtn.addEventListener('click', function() {
imageModal(imageElem.src) function previousImage(img) {
const allImages = Array.from(outputContainer.parentNode.querySelectorAll('.imgItem img'))
const index = allImages.indexOf(img)
return allImages.slice(0, index).reverse()[0]
}
function nextImage(img) {
const allImages = Array.from(outputContainer.parentNode.querySelectorAll('.imgItem img'))
const index = allImages.indexOf(img)
return allImages.slice(index + 1)[0]
}
function imageModalParameter(img) {
const previousImg = previousImage(img)
const nextImg = nextImage(img)
return {
src: img.src,
previous: previousImg ? () => imageModalParameter(previousImg) : undefined,
next: nextImg ? () => imageModalParameter(nextImg) : undefined,
}
}
imageModal(imageModalParameter(imageElem))
}) })
const req = Object.assign({}, reqBody, { const req = Object.assign({}, reqBody, {
@ -1099,7 +1122,7 @@ function createTask(task) {
function getCurrentUserRequest() { function getCurrentUserRequest() {
const numOutputsTotal = parseInt(numOutputsTotalField.value) const numOutputsTotal = parseInt(numOutputsTotalField.value)
const numOutputsParallel = parseInt(numOutputsParallelField.value) const numOutputsParallel = parseInt(numOutputsParallelField.value)
const seed = (randomSeedField.checked ? Math.floor(Math.random() * 10000000) : parseInt(seedField.value)) const seed = (randomSeedField.checked ? Math.floor(Math.random() * (2**32 - 1)) : parseInt(seedField.value))
const newTask = { const newTask = {
batchesDone: 0, batchesDone: 0,
@ -1287,13 +1310,15 @@ async function stopAllTasks() {
function updateInitialText() { function updateInitialText() {
if (document.querySelector('.imageTaskContainer') === null) { if (document.querySelector('.imageTaskContainer') === null) {
if (undoBuffer.length == 0) { if (undoBuffer.length > 0) {
previewTools.classList.add('displayNone') initialText.prepend(undoButton)
} }
previewTools.classList.add('displayNone')
initialText.classList.remove('displayNone') initialText.classList.remove('displayNone')
} else { } else {
initialText.classList.add('displayNone') initialText.classList.add('displayNone')
previewTools.classList.remove('displayNone') previewTools.classList.remove('displayNone')
document.querySelector('div.display-settings').prepend(undoButton)
} }
} }

View File

@ -15,10 +15,13 @@
* JSDoc style * JSDoc style
* @typedef {object} Parameter * @typedef {object} Parameter
* @property {string} id * @property {string} id
* @property {ParameterType} type * @property {keyof ParameterType} type
* @property {string} label * @property {string | (parameter: Parameter) => (HTMLElement | string)} label
* @property {?string} note * @property {string | (parameter: Parameter) => (HTMLElement | string) | undefined} note
* @property {(parameter: Parameter) => (HTMLElement | string) | undefined} render
* @property {string | undefined} icon
* @property {number|boolean|string} default * @property {number|boolean|string} default
* @property {boolean?} saveInAppConfig
*/ */
@ -118,6 +121,7 @@ var PARAMETERS = [
note: "starts the default browser on startup", note: "starts the default browser on startup",
icon: "fa-window-restore", icon: "fa-window-restore",
default: true, default: true,
saveInAppConfig: true,
}, },
{ {
id: "vram_usage_level", id: "vram_usage_level",
@ -179,6 +183,7 @@ var PARAMETERS = [
note: "Other devices on your network can access this web page", note: "Other devices on your network can access this web page",
icon: "fa-network-wired", icon: "fa-network-wired",
default: true, default: true,
saveInAppConfig: true,
}, },
{ {
id: "listen_port", id: "listen_port",
@ -188,7 +193,8 @@ var PARAMETERS = [
icon: "fa-anchor", icon: "fa-anchor",
render: (parameter) => { render: (parameter) => {
return `<input id="${parameter.id}" name="${parameter.id}" size="6" value="9000" onkeypress="preventNonNumericalInput(event)">` return `<input id="${parameter.id}" name="${parameter.id}" size="6" value="9000" onkeypress="preventNonNumericalInput(event)">`
} },
saveInAppConfig: true,
}, },
{ {
id: "use_beta_channel", id: "use_beta_channel",
@ -205,6 +211,7 @@ var PARAMETERS = [
note: "<b>Experimental! Can have bugs!</b> Use upcoming features (like LoRA) in our new engine. Please press Save, then restart the program after changing this.", note: "<b>Experimental! Can have bugs!</b> Use upcoming features (like LoRA) in our new engine. Please press Save, then restart the program after changing this.",
icon: "fa-bolt", icon: "fa-bolt",
default: false, default: false,
saveInAppConfig: true,
}, },
]; ];
@ -228,6 +235,10 @@ function sliderUpdate(event) {
} }
} }
/**
* @param {Parameter} parameter
* @returns {string | HTMLElement}
*/
function getParameterElement(parameter) { function getParameterElement(parameter) {
switch (parameter.type) { switch (parameter.type) {
case ParameterType.checkbox: case ParameterType.checkbox:
@ -243,29 +254,74 @@ function getParameterElement(parameter) {
case ParameterType.custom: case ParameterType.custom:
return parameter.render(parameter) return parameter.render(parameter)
default: default:
console.error(`Invalid type for parameter ${parameter.id}`); console.error(`Invalid type ${parameter.type} for parameter ${parameter.id}`);
return "ERROR: Invalid Type" return "ERROR: Invalid Type"
} }
} }
let parametersTable = document.querySelector("#system-settings .parameters-table") let parametersTable = document.querySelector("#system-settings .parameters-table")
/* fill in the system settings popup table */ /**
function initParameters() { * fill in the system settings popup table
PARAMETERS.forEach(parameter => { * @param {Array<Parameter> | undefined} parameters
var element = getParameterElement(parameter) * */
var note = parameter.note ? `<small>${parameter.note}</small>` : ""; function initParameters(parameters) {
var icon = parameter.icon ? `<i class="fa ${parameter.icon}"></i>` : ""; parameters.forEach(parameter => {
var newrow = document.createElement('div') const element = getParameterElement(parameter)
newrow.innerHTML = ` const elementWrapper = createElement('div')
<div>${icon}</div> if (element instanceof Node) {
<div><label for="${parameter.id}">${parameter.label}</label>${note}</div> elementWrapper.appendChild(element)
<div>${element}</div>` } else {
elementWrapper.innerHTML = element
}
const note = typeof parameter.note === 'function' ? parameter.note(parameter) : parameter.note
const noteElements = []
if (note) {
const noteElement = createElement('small')
if (note instanceof Node) {
noteElement.appendChild(note)
} else {
noteElement.innerHTML = note || ''
}
noteElements.push(noteElement)
}
const icon = parameter.icon ? [createElement('i', undefined, ['fa', parameter.icon])] : []
const label = typeof parameter.label === 'function' ? parameter.label(parameter) : parameter.label
const labelElement = createElement('label', { for: parameter.id })
if (label instanceof Node) {
labelElement.appendChild(label)
} else {
labelElement.innerHTML = label
}
const newrow = createElement(
'div',
{ 'data-setting-id': parameter.id, 'data-save-in-app-config': parameter.saveInAppConfig },
undefined,
[
createElement('div', undefined, undefined, icon),
createElement('div', undefined, undefined, [labelElement, ...noteElements]),
elementWrapper,
]
)
parametersTable.appendChild(newrow) parametersTable.appendChild(newrow)
parameter.settingsEntry = newrow parameter.settingsEntry = newrow
}) })
} }
initParameters() initParameters(PARAMETERS)
// listen to parameters from plugins
PARAMETERS.addEventListener('push', (...items) => {
initParameters(items)
if (items.find(item => item.saveInAppConfig)) {
console.log('Reloading app config for new parameters', items.map(p => p.id))
getAppConfig()
}
})
let vramUsageLevelField = document.querySelector('#vram_usage_level') let vramUsageLevelField = document.querySelector('#vram_usage_level')
let useCPUField = document.querySelector('#use_cpu') let useCPUField = document.querySelector('#use_cpu')
@ -330,9 +386,44 @@ async function getAppConfig() {
document.querySelector("#lora_alpha_container").style.display = (testDiffusers.checked && loraModelField.value !== "" ? '' : 'none') document.querySelector("#lora_alpha_container").style.display = (testDiffusers.checked && loraModelField.value !== "" ? '' : 'none')
} }
Array.from(parametersTable.children).forEach(parameterRow => {
if (parameterRow.dataset.settingId in config && parameterRow.dataset.saveInAppConfig === 'true') {
const configValue = config[parameterRow.dataset.settingId]
const parameterElement = document.getElementById(parameterRow.dataset.settingId) ||
parameterRow.querySelector('input') || parameterRow.querySelector('select')
switch (parameterElement?.tagName) {
case 'INPUT':
if (parameterElement.type === 'checkbox') {
parameterElement.checked = configValue
} else {
parameterElement.value = configValue
}
parameterElement.dispatchEvent(new Event('change'))
break
case 'SELECT':
if (Array.isArray(configValue)) {
Array.from(parameterElement.options).forEach(option => {
if (configValue.includes(option.value || option.text)) {
option.selected = true
}
})
} else {
parameterElement.value = configValue
}
parameterElement.dispatchEvent(new Event('change'))
break
}
}
})
console.log('get config status response', config) console.log('get config status response', config)
return config
} catch (e) { } catch (e) {
console.log('get config status error', e) console.log('get config status error', e)
return {}
} }
} }
@ -492,16 +583,43 @@ saveSettingsBtn.addEventListener('click', function() {
alert('The network port must be a number from 1 to 65535') alert('The network port must be a number from 1 to 65535')
return return
} }
let updateBranch = (useBetaChannelField.checked ? 'beta' : 'main') const updateBranch = (useBetaChannelField.checked ? 'beta' : 'main')
changeAppConfig({
const updateAppConfigRequest = {
'render_devices': getCurrentRenderDeviceSelection(), 'render_devices': getCurrentRenderDeviceSelection(),
'update_branch': updateBranch, 'update_branch': updateBranch,
'ui_open_browser_on_start': uiOpenBrowserOnStartField.checked, }
'listen_to_network': listenToNetworkField.checked,
'listen_port': listenPortField.value, Array.from(parametersTable.children).forEach(parameterRow => {
'test_diffusers': testDiffusers.checked if (parameterRow.dataset.saveInAppConfig === 'true') {
}) const parameterElement = document.getElementById(parameterRow.dataset.settingId) ||
saveSettingsBtn.classList.add('active') parameterRow.querySelector('input') || parameterRow.querySelector('select')
asyncDelay(300).then(() => saveSettingsBtn.classList.remove('active'))
switch (parameterElement?.tagName) {
case 'INPUT':
if (parameterElement.type === 'checkbox') {
updateAppConfigRequest[parameterRow.dataset.settingId] = parameterElement.checked
} else {
updateAppConfigRequest[parameterRow.dataset.settingId] = parameterElement.value
}
break
case 'SELECT':
if (parameterElement.multiple) {
updateAppConfigRequest[parameterRow.dataset.settingId] = Array.from(parameterElement.options)
.filter(option => option.selected)
.map(option => option.value || option.text)
} else {
updateAppConfigRequest[parameterRow.dataset.settingId] = parameterElement.value
}
break
default:
console.error(`Setting parameter ${parameterRow.dataset.settingId} couldn't be saved to app.config - element #${parameter.id} is a <${parameterElement?.tagName} /> instead of a <input /> or a <select />!`)
break
}
}
}) })
const savePromise = changeAppConfig(updateAppConfigRequest)
saveSettingsBtn.classList.add('active')
Promise.all([savePromise, asyncDelay(300)]).then(() => saveSettingsBtn.classList.remove('active'))
})

View File

@ -683,14 +683,16 @@ class ServiceContainer {
* @param {string} tag * @param {string} tag
* @param {object} attributes * @param {object} attributes
* @param {string | Array<string>} classes * @param {string | Array<string>} classes
* @param {string | HTMLElement | Array<string | HTMLElement>} * @param {string | Node | Array<string | Node>}
* @returns {HTMLElement} * @returns {HTMLElement}
*/ */
function createElement(tagName, attributes, classes, textOrElements) { function createElement(tagName, attributes, classes, textOrElements) {
const element = document.createElement(tagName) const element = document.createElement(tagName)
if (attributes) { if (attributes) {
Object.entries(attributes).forEach(([key, value]) => { Object.entries(attributes).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
element.setAttribute(key, value) element.setAttribute(key, value)
}
}); });
} }
if (classes) { if (classes) {
@ -699,7 +701,7 @@ function createElement(tagName, attributes, classes, textOrElements) {
if (textOrElements) { if (textOrElements) {
const children = Array.isArray(textOrElements) ? textOrElements : [textOrElements] const children = Array.isArray(textOrElements) ? textOrElements : [textOrElements]
children.forEach(textOrElem => { children.forEach(textOrElem => {
if (textOrElem instanceof HTMLElement) { if (textOrElem instanceof Node) {
element.appendChild(textOrElem) element.appendChild(textOrElem)
} else { } else {
element.appendChild(document.createTextNode(textOrElem)) element.appendChild(document.createTextNode(textOrElem))
@ -708,3 +710,19 @@ function createElement(tagName, attributes, classes, textOrElements) {
} }
return element return element
} }
/**
* Add a listener for arrays
* @param {keyof Array} method
* @param {(args) => {}} callback
*/
Array.prototype.addEventListener = function(method, callback) {
const originalFunction = this[method]
if (originalFunction) {
this[method] = function() {
console.log(`Array.${method}()`, arguments)
originalFunction.apply(this, arguments)
callback.apply(this, arguments)
}
}
}