Merge ab547d10bfc25e7ce9c6ff37fde5a89af4bd326e into 5c7625c425f12330eec34870511c31667d9fe349

This commit is contained in:
ManInDark 2025-04-04 20:51:05 +01:00 committed by GitHub
commit e85eb98d1f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 3496 additions and 14 deletions

View File

@ -1120,3 +1120,29 @@ ExifReader is licensed under the Mozilla Public License:
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
Masonry
=======
https://masonry.desandro.com/
Masonry is licensed under the MIT license:
The MIT License (MIT)
Copyright © 2023 David DeSandro
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -15,9 +15,9 @@ Does not require technical knowledge, does not require pre-installed software. 1
Click the download button for your operating system:
<p float="left">
<a href="https://github.com/cmdr2/stable-diffusion-ui/releases/latest/download/Easy-Diffusion-Linux.zip"><img src="https://github.com/cmdr2/stable-diffusion-ui/raw/main/media/download-linux.png" width="200" /></a>
<a href="https://github.com/cmdr2/stable-diffusion-ui/releases/latest/download/Easy-Diffusion-Mac.zip"><img src="https://github.com/cmdr2/stable-diffusion-ui/raw/main/media/download-mac.png" width="200" /></a>
<a href="https://github.com/cmdr2/stable-diffusion-ui/releases/latest/download/Easy-Diffusion-Windows.exe"><img src="https://github.com/cmdr2/stable-diffusion-ui/raw/main/media/download-win.png" width="200" /></a>
<a href="https://github.com/easydiffusion/easydiffusion/releases/download/v2.5.41a/Easy-Diffusion-Windows.exe"><img src="https://github.com/easydiffusion/easydiffusion/raw/main/media/download-win.png" width="200" /></a>
<a href="https://github.com/easydiffusion/easydiffusion/releases/download/v2.5.41a/Easy-Diffusion-Linux.zip"><img src="https://github.com/easydiffusion/easydiffusion/raw/main/media/download-linux.png" width="200" /></a>
<a href="https://github.com/easydiffusion/easydiffusion/releases/download/v2.5.41a/Easy-Diffusion-Mac.zip"><img src="https://github.com/easydiffusion/easydiffusion/raw/main/media/download-mac.png" width="200" /></a>
</p>
**Hardware requirements:**

View File

@ -1,12 +1,14 @@
from typing import List
from fastapi import Depends, FastAPI, HTTPException, Response, File
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from easydiffusion.easydb import crud, models, schemas
from easydiffusion.easydb.database import SessionLocal, engine
from requests.compat import urlparse
from os.path import abspath
import base64, json
@ -92,7 +94,28 @@ def init():
result.data = base64.encodestring(result.data)
return result
@server_api.get("/image/{image_path:path}")
def get_image(image_path: str, db: Session = Depends(get_db)):
from easydiffusion.easydb.mappings import GalleryImage
image_path = str(abspath(image_path))
try:
image = db.query(GalleryImage).filter(GalleryImage.path == image_path).first()
return FileResponse(image.path)
except Exception as e:
raise HTTPException(status_code=404, detail="Image not found")
@server_api.get("/all_images")
def get_all_images(prompt: str = "", model: str = "", page: int = 0, images_per_page: int = 50, workspace : str = "default", db: Session = Depends(get_db)):
from easydiffusion.easydb.mappings import GalleryImage
images = db.query(GalleryImage).filter(GalleryImage.workspace == workspace).order_by(GalleryImage.time_created.desc())
if prompt != "":
images = images.filter(GalleryImage.prompt.like("%"+prompt+"%"))
if model != "":
images = images.filter(GalleryImage.use_stable_diffusion_model.like("%"+model+"%"))
images = images.offset(page*images_per_page).limit(images_per_page)
return images.all()
def get_filename_from_url(url):
path = urlparse(url).path
name = path[path.rfind('/')+1:]

View File

@ -0,0 +1,82 @@
from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql import func
Base = declarative_base()
class GalleryImage(Base):
__tablename__ = 'images'
path = Column(String, primary_key=True)
seed = Column(Integer)
use_stable_diffusion_model = Column(String)
clip_skip = Column(Boolean)
use_vae_model = Column(String)
sampler_name = Column(String)
width = Column(Integer)
height = Column(Integer)
num_inference_steps = Column(Integer)
guidance_scale = Column(Float)
lora = Column(String)
use_hypernetwork_model = Column(String)
tiling = Column(String)
use_face_correction = Column(String)
use_upscale = Column(String)
prompt = Column(String)
negative_prompt = Column(String)
workspace = Column(String, server_default="default")
time_created = Column(DateTime(timezone=True), server_default=func.now())
nsfw = Column(Boolean, server_default=None)
def __repr__(self):
return "<GalleryImage(path='%s', seed='%s', use_stable_diffusion_model='%s', clip_skip='%s', use_vae_model='%s', sampler_name='%s', width='%s', height='%s', num_inference_steps='%s', guidance_scale='%s', lora='%s', use_hypernetwork_model='%s', tiling='%s', use_face_correction='%s', use_upscale='%s', prompt='%s', negative_prompt='%s')>" % (
self.path, self.seed, self.use_stable_diffusion_model, self.clip_skip, self.use_vae_model, self.sampler_name, self.width, self.height, self.num_inference_steps, self.guidance_scale, self.lora, self.use_hypernetwork_model, self.tiling, self.use_face_correction, self.use_upscale, self.prompt, self.negative_prompt)
def htmlForm(self) -> str:
return "<div class='panel-box'><p>Path: " + str(self.path) + "</p>" + \
"<p>Seed: " + str(self.seed) + "</p>" + \
"<p>Stable Diffusion Model: " + str(self.use_stable_diffusion_model) + "</p>" + \
"<p>Prompt: " + str(self.prompt) + "</p>" + \
"<p>Negative Prompt: " + str(self.negative_prompt) + "</p>" + \
"<p>Clip Skip: " + str(self.clip_skip) + "</p>" + \
"<p>VAE Model: " + str(self.use_vae_model) + "</p>" + \
"<p>Sampler: " + str(self.sampler_name) + "</p>" + \
"<p>Size: " + str(self.height) + "x" + str(self.width) + "</p>" + \
"<p>Inference Steps: " + str(self.num_inference_steps) + "</p>" + \
"<p>Guidance Scale: " + str(self.guidance_scale) + "</p>" + \
"<p>LoRA: " + str(self.lora) + "</p>" + \
"<p>Hypernetwork: " + str(self.use_hypernetwork_model) + "</p>" + \
"<p>Tiling: " + str(self.tiling) + "</p>" + \
"<p>Face Correction: " + str(self.use_face_correction) + "</p>" + \
"<p>Upscale: " + str(self.use_upscale) + "</p>" + \
"<p>Time Created: " + str(self.time_created) + "</p>" + \
"<p>NSFW: " + str(self.nsfw) + "</p></div>"
def settingsJSON(self) -> str:
# some are still missing: prompt strength, lora
json = {
"numOutputsTotal": 1,
"seed": self.seed,
"reqBody": {
"prompt": self.prompt,
"negative_prompt": self.negative_prompt,
"width": self.width,
"height": self.height,
"seed": self.seed,
"num_inference_steps": self.num_inference_steps,
"guidance_scale": self.guidance_scale,
"use_face_correction": self.use_face_correction,
"use_upscale": self.use_upscale,
"sampler_name": self.sampler_name,
"use_stable_diffusion_model": self.use_stable_diffusion_model,
"clip_skip": self.clip_skip,
"tiling": self.tiling,
"use_vae_model": self.use_vae_model,
"use_hypernetwork_model": self.use_hypernetwork_model
}}
from json import dumps
return dumps(json)
from easydiffusion.easydb.database import engine
GalleryImage.metadata.create_all(engine)

View File

@ -155,6 +155,10 @@ def init():
def read_root():
return FileResponse(os.path.join(app.SD_UI_DIR, "index.html"), headers=NOCACHE_HEADERS)
@server_api.get("/gallery-image.html")
def read_gallery_image():
return FileResponse(os.path.join(app.SD_UI_DIR, "gallery-image.html"), headers=NOCACHE_HEADERS)
@server_api.on_event("shutdown")
def shutdown_event(): # Signal render thread to close on shutdown
task_manager.current_state_error = SystemExit("Application shutting down.")

View File

@ -96,6 +96,7 @@ class RenderTaskData(TaskData):
clip_skip: bool = False
codeformer_upscale_faces: bool = False
codeformer_fidelity: float = 0.5
use_gallery: str = None
class MergeRequest(BaseModel):

View File

@ -159,6 +159,49 @@ def save_images_to_disk(
output_quality=output_format.output_quality,
output_lossless=output_format.output_lossless,
)
for i in range(len(filtered_images)):
path_i = f"{os.path.join(save_dir_path, make_filename(i))}.{output_format.output_format.lower()}"
def createLoraString(metadata_entries, i):
if metadata_entries[i]["use_lora_model"] is None:
return "None"
elif isinstance(metadata_entries[i]["use_lora_model"], list):
loraString = ""
for j in range(len(metadata_entries[i]["use_lora_model"])):
loraString += metadata_entries[i]["use_lora_model"][j] + ":" + str(metadata_entries[i]["lora_alpha"][j]) + " "
return loraString.strip()
else:
return metadata_entries[i]["use_lora_model"] + ":" + str(metadata_entries[i]["lora_alpha"])
from easydiffusion.easydb.mappings import GalleryImage
from easydiffusion.easydb.database import SessionLocal
if task_data.use_gallery != None:
session = SessionLocal()
session.add(GalleryImage(
path = path_i,
seed = metadata_entries[i]["seed"],
use_stable_diffusion_model = metadata_entries[i]["use_stable_diffusion_model"],
clip_skip = metadata_entries[i]["clip_skip"],
use_vae_model = metadata_entries[i]["use_vae_model"],
sampler_name = metadata_entries[i]["sampler_name"],
width = metadata_entries[i]["width"],
height = metadata_entries[i]["height"],
num_inference_steps = metadata_entries[i]["num_inference_steps"],
guidance_scale = metadata_entries[i]["guidance_scale"],
lora = createLoraString(metadata_entries, i),
use_hypernetwork_model = metadata_entries[i]["use_hypernetwork_model"] if "use_hypernetwork_model" in metadata_entries[i] else None,
tiling = metadata_entries[i]["tiling"],
use_face_correction = metadata_entries[i]["use_face_correction"],
use_upscale = metadata_entries[i]["use_upscale"],
prompt = metadata_entries[i]["prompt"],
negative_prompt = metadata_entries[i]["negative_prompt"],
workspace = task_data.use_gallery
))
session.commit()
session.close()
if save_data.metadata_output_format:
for metadata_output_format in save_data.metadata_output_format.split(","):
if metadata_output_format.lower() in ["json", "txt", "embed"]:

14
ui/gallery-image.html Normal file
View File

@ -0,0 +1,14 @@
<html>
<head>
<link rel="stylesheet" href="/media/css/single-gallery.css">
</head>
<body>
<div>
<button id="use_these_settings" class="primaryButton">Use these settings</button>
<button id="use_as_input" class="primaryButton">Use as Input</button>
</div>
<img id="focusimg">
<div id="focusbox" class="panel-box">
</div>
</body></html>

View File

@ -27,6 +27,7 @@
<script src="/media/js/marked.min.js"></script>
<script src="/media/js/croppr.js"></script>
<script src="/media/js/exif-reader.js"></script>
<script src="/media/js/masonry.pkgd.js"></script>
</head>
<body>
<div id="container">
@ -58,6 +59,9 @@
<span id="tab-about" class="tab">
<span><i class="fa fa-comments icon"></i> Help & Community</span>
</span>
<span id="tab-gallery" class="tab">
<span><i class="fa fa-images icon"></i> Gallery</span>
</span>
</div>
</div>
@ -670,6 +674,34 @@
</div>
</div>
</div>
<div id="tab-content-gallery" class="tab-content">
<div id="gallery-search">
<button id="gallery-prev" class="primaryButton"><i class="fa-solid fa-arrow-left"></i></button>
<input id="gallery-prompt-search" type="search" placeholder="Search for a prompt..."></input>
<input id="gallery-model-search" type="search" placeholder="Search for a model..."></input>
<label for="gallery-page">Page:</label>
<input id="gallery-page" name="Page" value="0" size="1" onkeypress="gallery_keyDown_handler(event)">
<input id="gallery-thumbnail-size" name="gallery-thumbnail-size" class="editor-slider" type="range" value="5.5" min="3" max="20" step="0.05">
<button class="primaryButton" id="gallery-refresh" onclick="refreshGallery(true)">Load</button>
<button id="gallery-next" class="primaryButton"><i class="fa-solid fa-arrow-right"></i></button>
</div>
<div class="gallery">
<div class="gallery-container" id="imagecontainer"></div>
</div>
<dialog id="gallery-imginfo">
<div id="gallery-imginfo-header" class="dialog-header">
<div id="gallery-imginfo-header-left" class="dialog-header-left">
<h4>Image Settings</h4>
<span></span>
</div>
<div id="gallery-imginfo-header-right">
<i id="gallery-imginfo-close-button" class="fa-solid fa-xmark fa-lg"></i>
</div>
</div>
<div id="gallery-imginfo-content">
</div>
</dialog>
</div>
</div>
<div class="popup" id="splash-screen" data-version="1">

View File

@ -1058,6 +1058,9 @@ input::file-selector-button {
.input-toggle > input:checked + label {
background: var(--accent-color);
}
.input-toggle > input:disabled + label {
background: var(--background-color1);
}
.input-toggle > input:checked + label:before {
right: calc(var(--input-border-size) + var(--input-switch-padding));
opacity: 1;
@ -2053,3 +2056,57 @@ div#enlarge-buttons {
padding-top: 6pt;
color: var(--small-label-color);
}
/* Gallery CSS */
button:disabled {
background-color: var(--secondary-button-background);
}
#gallery-search {
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
}
#gallery-search>* {
margin: 0.5em;
}
#imagecontainer {
margin: auto;
}
.gallery-container {
}
.gallery-image img {
width: var(--gallery-width);
}
.gallery-image {
position: relative;
width: var(--gallery-width);
margin-bottom: 6px;
}
.gallery-image-hover {
position: absolute;
right: 15px;
top: 15px;
opacity: 0;
text-align: right;
}
.gallery-image-hover button {
margin-bottom: 5px;
}
.gallery-image:hover .gallery-image-hover {
opacity: 1;
}
#tab-content-gallery>* {
margin: 8px;
}

View File

@ -0,0 +1,42 @@
@import url("/media/css/themes.css");
@import url("/media/css/main.css");
body {
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--background-color1);
}
img {
border-radius: 5px;
}
p, h1, h2, h3, h4, h5, h6 {
color: var(--text-color);
cursor: default;
margin: 6px;
}
::-moz-selection {
/* Code for Firefox */
color: none;
background: none;
}
::selection {
color: none;
background: none;
}
button {
margin: 8px;
}
div {
margin: 16px;
}
:disabled {
color: gray;
}

View File

@ -14,6 +14,7 @@ const SETTINGS_IDS_LIST = [
"num_outputs_parallel",
"stable_diffusion_model",
"clip_skip",
"use_gallery",
"vae_model",
"sampler_name",
"scheduler_name",
@ -58,6 +59,7 @@ const SETTINGS_IDS_LIST = [
"tree_toggle",
"json_toggle",
"extract_lora_from_prompt",
"gallery-thumbnail-size",
"embedding-card-size-selector",
"lora_model",
"enable_vae_tiling",

View File

@ -165,6 +165,15 @@ let embeddingsModeField = document.querySelector("#embeddings-mode")
let embeddingsCardSizeSelector = document.querySelector("#embedding-card-size-selector")
let addEmbeddingsThumb = document.querySelector("#add-embeddings-thumb")
let addEmbeddingsThumbInput = document.querySelector("#add-embeddings-thumb-input")
let galleryImginfoDialog = document.querySelector("#gallery-imginfo")
let galleryThumbnailSize = document.querySelector("#gallery-thumbnail-size")
let galleryImginfoDialogContent = document.querySelector("#gallery-imginfo-content")
let galleryPageField = document.querySelector("#gallery-page")
let galleryPrevBtn = document.querySelector("#gallery-prev")
let galleryNextBtn = document.querySelector("#gallery-next")
let galleryPromptSearchField = document.querySelector("#gallery-prompt-search")
let galleryModelSearchField = document.querySelector("#gallery-model-search")
let galleryImageContainer = document.querySelector("#imagecontainer")
let positiveEmbeddingText = document.querySelector("#positive-embedding-text")
let negativeEmbeddingText = document.querySelector("#negative-embedding-text")
@ -209,6 +218,7 @@ let undoButton = document.querySelector("#undo")
let undoBuffer = []
const UNDO_LIMIT = 20
const MAX_IMG_UNDO_ENTRIES = 5
var GALLERY_NAME="default"
let IMAGE_STEP_SIZE = 64
@ -1085,14 +1095,361 @@ function makeImage() {
newTaskRequests.forEach(setEmbeddings)
newTaskRequests.forEach(createTask)
updateTitle()
updateInitialText()
}
const countBeforeBanner = localStorage.getItem("countBeforeBanner") || 1
if (countBeforeBanner <= 0) {
// supportBanner.classList.remove("displayNone")
async function onIdle() {
const serverCapacity = SD.serverCapacity
if (pauseClient === true) {
await resumeClient()
}
for (const taskEntry of getUncompletedTaskEntries()) {
if (SD.activeTasks.size >= serverCapacity) {
break
}
const task = htmlTaskMap.get(taskEntry)
if (!task) {
const taskStatusLabel = taskEntry.querySelector(".taskStatusLabel")
taskStatusLabel.style.display = "none"
continue
}
await onTaskStart(task)
}
}
function getTaskUpdater(task, reqBody, outputContainer) {
const outputMsg = task["outputMsg"]
const progressBar = task["progressBar"]
const progressBarInner = progressBar.querySelector("div")
const batchCount = task.batchCount
let lastStatus = undefined
return async function(event) {
if (this.status !== lastStatus) {
lastStatus = this.status
switch (this.status) {
case SD.TaskStatus.pending:
task["taskStatusLabel"].innerText = "Pending"
task["taskStatusLabel"].classList.add("waitingTaskLabel")
break
case SD.TaskStatus.waiting:
task["taskStatusLabel"].innerText = "Waiting"
task["taskStatusLabel"].classList.add("waitingTaskLabel")
task["taskStatusLabel"].classList.remove("activeTaskLabel")
break
case SD.TaskStatus.processing:
case SD.TaskStatus.completed:
task["taskStatusLabel"].innerText = "Processing"
task["taskStatusLabel"].classList.add("activeTaskLabel")
task["taskStatusLabel"].classList.remove("waitingTaskLabel")
break
case SD.TaskStatus.stopped:
break
case SD.TaskStatus.failed:
if (!SD.isServerAvailable()) {
logError(
"Stable Diffusion is still starting up, please wait. If this goes on beyond a few minutes, Stable Diffusion has probably crashed. Please check the error message in the command-line window.",
event,
outputMsg
)
} else if (typeof event?.response === "object") {
let msg = "Stable Diffusion had an error reading the response:<br/><pre>"
if (this.exception) {
msg += `Error: ${this.exception.message}<br/>`
}
try {
// 'Response': body stream already read
msg += "Read: " + (await event.response.text())
} catch (e) {
msg += "Unexpected end of stream. "
}
const bufferString = event.reader.bufferedString
if (bufferString) {
msg += "Buffered data: " + bufferString
}
msg += "</pre>"
logError(msg, event, outputMsg)
}
break
}
}
if ("update" in event) {
const stepUpdate = event.update
if (!("step" in stepUpdate)) {
return
}
// task.instances can be a mix of different tasks with uneven number of steps (Render Vs Filter Tasks)
const overallStepCount =
task.instances.reduce(
(sum, instance) =>
sum +
(instance.isPending
? Math.max(0, instance.step || stepUpdate.step) /
(instance.total_steps || stepUpdate.total_steps)
: 1),
0 // Initial value
) * stepUpdate.total_steps // Scale to current number of steps.
const totalSteps = task.instances.reduce(
(sum, instance) => sum + (instance.total_steps || stepUpdate.total_steps),
stepUpdate.total_steps * (batchCount - task.batchesDone) // Initial value at (unstarted task count * Nbr of steps)
)
const percent = Math.min(100, 100 * (overallStepCount / totalSteps)).toFixed(0)
const timeTaken = stepUpdate.step_time // sec
const stepsRemaining = Math.max(0, totalSteps - overallStepCount)
const timeRemaining = timeTaken < 0 ? "" : millisecondsToStr(stepsRemaining * timeTaken * 1000)
outputMsg.innerHTML = `Batch ${task.batchesDone} of ${batchCount}. Generating image(s): ${percent}%. Time remaining (approx): ${timeRemaining}`
outputMsg.style.display = "block"
progressBarInner.style.width = `${percent}%`
updateTitle()
if (stepUpdate.output) {
showImages(reqBody, stepUpdate, outputContainer, true)
}
}
}
}
function abortTask(task) {
if (!task.isProcessing) {
return false
}
task.isProcessing = false
task.progressBar.classList.remove("active")
task["taskStatusLabel"].style.display = "none"
task["stopTask"].innerHTML = '<i class="fa-solid fa-trash-can"></i> Remove'
if (!task.instances?.some((r) => r.isPending)) {
return
}
task.instances.forEach((instance) => {
try {
instance.abort()
} catch (e) {
console.error(e)
}
})
if (task.batchesDone > 0) {
document.title = "Easy Diffusion"
}
}
function onTaskErrorHandler(task, reqBody, instance, reason) {
if (!task.isProcessing) {
return
}
console.log("Render request %o, Instance: %o, Error: %s", reqBody, instance, reason)
abortTask(task)
const outputMsg = task["outputMsg"]
logError(
"Stable Diffusion had an error. Please check the logs in the command-line window. <br/><br/>" +
reason +
"<br/><pre>" +
reason.stack +
"</pre>",
task,
outputMsg
)
setStatus("request", "error", "error")
}
function onTaskCompleted(task, reqBody, instance, outputContainer, stepUpdate) {
if (typeof stepUpdate === "object") {
if (stepUpdate.status === "succeeded") {
showImages(reqBody, stepUpdate, outputContainer, false)
} else {
task.isProcessing = false
const outputMsg = task["outputMsg"]
let msg = ""
if ("detail" in stepUpdate && typeof stepUpdate.detail === "string" && stepUpdate.detail.length > 0) {
msg = stepUpdate.detail
if (msg.toLowerCase().includes("out of memory")) {
msg += `<br/><br/>
<b>Suggestions</b>:
<br/>
1. If you have set an initial image, please try reducing its dimension to ${MAX_INIT_IMAGE_DIMENSION}x${MAX_INIT_IMAGE_DIMENSION} or smaller.<br/>
2. Try picking a lower level in the '<em>GPU Memory Usage</em>' setting (in the '<em>Settings</em>' tab).<br/>
3. Try generating a smaller image.<br/>`
} else if (msg.includes("DefaultCPUAllocator: not enough memory")) {
msg += `<br/><br/>
Reason: Your computer is running out of system RAM!
<br/><br/>
<b>Suggestions</b>:
<br/>
1. Try closing unnecessary programs and browser tabs.<br/>
2. If that doesn't help, please increase your computer's virtual memory by following these steps for
<a href="https://www.ibm.com/docs/en/opw/8.2.0?topic=tuning-optional-increasing-paging-file-size-windows-computers" target="_blank">Windows</a> or
<a href="https://linuxhint.com/increase-swap-space-linux/" target="_blank">Linux</a>.<br/>
3. Try restarting your computer.<br/>`
}
} else {
msg = `Unexpected Read Error:<br/><pre>StepUpdate: ${JSON.stringify(stepUpdate, undefined, 4)}</pre>`
}
logError(msg, stepUpdate, outputMsg)
}
}
if (task.isProcessing && task.batchesDone < task.batchCount) {
task["taskStatusLabel"].innerText = "Pending"
task["taskStatusLabel"].classList.add("waitingTaskLabel")
task["taskStatusLabel"].classList.remove("activeTaskLabel")
return
}
if ("instances" in task && task.instances.some((ins) => ins != instance && ins.isPending)) {
return
}
task.isProcessing = false
task["stopTask"].innerHTML = '<i class="fa-solid fa-trash-can"></i> Remove'
task["taskStatusLabel"].style.display = "none"
let time = millisecondsToStr(Date.now() - task.startTime)
if (task.batchesDone == task.batchCount) {
if (!task.outputMsg.innerText.toLowerCase().includes("error")) {
task.outputMsg.innerText = `Processed ${task.numOutputsTotal} images in ${time}`
}
task.progressBar.style.height = "0px"
task.progressBar.style.border = "0px solid var(--background-color3)"
task.progressBar.classList.remove("active")
setStatus("request", "done", "success")
} else {
localStorage.setItem("countBeforeBanner", countBeforeBanner - 1)
}
if (randomSeedField.checked) {
seedField.value = task.seed
}
if (SD.activeTasks.size > 0) {
return
}
const uncompletedTasks = getUncompletedTaskEntries()
if (uncompletedTasks && uncompletedTasks.length > 0) {
return
}
if (pauseClient) {
resumeBtn.click()
}
renderButtons.style.display = "none"
renameMakeImageButton()
if (isSoundEnabled()) {
playSound()
}
updateTitle()
}
function updateTitle() {
let all_tasks = [...document.querySelectorAll("div .imageTaskContainer").entries()].map(c => htmlTaskMap.get(c[1]))
let tasks_to_be_run = all_tasks.filter(task => task.isProcessing)
let img_remaining_per_task = tasks_to_be_run.map(task => { return task.numOutputsTotal - Math.max(0, (task.instances || []).reduce((total, value) => {
if (value.status === 'completed') {
return total + 1
} else {
return total
}
}, 0) * task.reqBody.num_outputs) } )
let img_remaining = img_remaining_per_task.reduce((total, value) => total + value, 0);
if (img_remaining > 0) {
document.title = `${img_remaining} - Easy Diffusion`;
} else {
document.title = "Completed - Easy Diffusion"
}
}
async function onTaskStart(task) {
if (!task.isProcessing || task.batchesDone >= task.batchCount) {
return
}
if (typeof task.startTime !== "number") {
task.startTime = Date.now()
}
if (!("instances" in task)) {
task["instances"] = []
}
task["stopTask"].innerHTML = '<i class="fa-solid fa-circle-stop"></i> Stop'
task["taskStatusLabel"].innerText = "Starting"
task["taskStatusLabel"].classList.add("waitingTaskLabel")
let newTaskReqBody = task.reqBody
if (task.batchCount > 1) {
// Each output render batch needs it's own task reqBody instance to avoid altering the other runs after they are completed.
newTaskReqBody = Object.assign({}, task.reqBody)
if (task.batchesDone == task.batchCount - 1) {
// Last batch of the task
// If the number of parallel jobs is no factor of the total number of images, the last batch must create less than "parallel jobs count" images
// E.g. with numOutputsTotal = 6 and num_outputs = 5, the last batch shall only generate 1 image.
newTaskReqBody.num_outputs = task.numOutputsTotal - task.reqBody.num_outputs * (task.batchCount - 1)
}
}
const startSeed = task.seed || newTaskReqBody.seed
const genSeeds = Boolean(
typeof newTaskReqBody.seed !== "number" || (newTaskReqBody.seed === task.seed && task.numOutputsTotal > 1)
)
if (genSeeds) {
newTaskReqBody.seed = parseInt(startSeed) + task.batchesDone * task.reqBody.num_outputs
}
// Update the seed *before* starting the processing so it's retained if user stops the task
if (randomSeedField.checked) {
seedField.value = task.seed
}
const outputContainer = document.createElement("div")
outputContainer.className = "img-batch"
task.outputContainer.insertBefore(outputContainer, task.outputContainer.firstChild)
const eventInfo = { reqBody: newTaskReqBody }
const callbacksPromises = PLUGINS["TASK_CREATE"].map((hook) => {
if (typeof hook !== "function") {
console.error("The provided TASK_CREATE hook is not a function. Hook: %o", hook)
return Promise.reject(new Error("hook is not a function."))
}
try {
return Promise.resolve(hook.call(task, eventInfo))
} catch (err) {
console.error(err)
return Promise.reject(err)
}
})
await Promise.allSettled(callbacksPromises)
let instance = eventInfo.instance
if (!instance) {
const factory = PLUGINS.OUTPUTS_FORMATS.get(eventInfo.reqBody?.output_format || newTaskReqBody.output_format)
if (factory) {
instance = await Promise.resolve(factory(eventInfo.reqBody || newTaskReqBody))
}
if (!instance) {
console.error(
`${factory ? "Factory " + String(factory) : "No factory defined"} for output format ${eventInfo.reqBody
?.output_format || newTaskReqBody.output_format}. Instance is ${instance ||
"undefined"}. Using default renderer.`
)
instance = new SD.RenderTask(eventInfo.reqBody || newTaskReqBody)
}
}
task["instances"].push(instance)
task.batchesDone++
instance.enqueue(getTaskUpdater(task, newTaskReqBody, outputContainer)).then(
(renderResult) => {
onTaskCompleted(task, newTaskReqBody, instance, outputContainer, renderResult)
},
(reason) => {
onTaskErrorHandler(task, newTaskReqBody, instance, reason)
}
)
setStatus("request", "fetching..")
renderButtons.style.display = "flex"
renameMakeImageButton()
updateInitialText()
}
/* Hover effect for the init image in the task list */
@ -1352,6 +1709,7 @@ function getCurrentUserRequest() {
output_quality: parseInt(outputQualityField.value),
output_lossless: outputLosslessField.checked,
metadata_output_format: metadataOutputFormatField.value,
use_gallery: useGalleryField.checked?GALLERY_NAME:null,
original_prompt: promptField.value,
active_tags: activeTags.map((x) => x.name),
inactive_tags: activeTags.filter((tag) => tag.inactive === true).map((x) => x.name),
@ -1851,6 +2209,7 @@ function onDimensionChange() {
diskPathField.disabled = !saveToDiskField.checked
metadataOutputFormatField.disabled = !saveToDiskField.checked
useGalleryField.disabled = !saveToDiskField.checked
gfpganModelField.disabled = !useFaceCorrectionField.checked
useFaceCorrectionField.addEventListener("change", function(e) {
@ -3115,3 +3474,272 @@ document.addEventListener("on_all_tasks_complete", (e) => {
playSound()
}
})
/* Gallery JS */
const IMAGE_INFO = {
"Prompt": "prompt",
"Negative Prompt": "negative_prompt",
"Seed": "seed",
"Time": "time_created",
"Model": "use_stable_diffusion_model",
"VAE Model": "use_vae_model",
"Hypernetwork": "use_hypernetwork_model",
"LORA": "lora",
"Path": "path",
"Width": "width",
"Height": "height",
"Steps": "num_inference_steps",
"Sampler": "sampler_name",
"Guidance Scale": "guidance_scale",
"Tiling": "tiling",
"Upscaler": "use_upscale",
"Face Correction": "use_face_correction",
"Clip Skip": "clip_skip",
}
const IGNORE_TOKENS = ["None", "none", "Null", "null", "false", "False", null]
function openImageInNewTab(img, reqData) {
let w = window.open("/gallery-image.html")
w.addEventListener("DOMContentLoaded", () => {
let fimg = w.document.getElementById("focusimg")
fimg.src = img.src
w.document.body.classList.add(themeField.value)
w.document.getElementById("use_these_settings").addEventListener("click", () => {
restoreTaskToUI({
batchCount: 1,
numOutputsTotal: 1,
reqBody: reqData
})
})
w.document.getElementById("use_as_input").addEventListener("click", () => {
onUseURLasInput(img.src)
showToast("Loaded image as EasyDiffusion input image", 5000, false, w.document)
})
let table = w.document.createElement("table")
galleryDetailTable(table, reqData)
w.document.getElementById("focusbox").replaceChildren(table)
document.dispatchEvent(new CustomEvent("newGalleryWindow", { detail: w }))
})
}
function galleryDetailTable(table, reqData) {
for (const [label, key] of Object.entries(IMAGE_INFO)) {
if (IGNORE_TOKENS.findIndex( k => (k == reqData[key])) == -1) {
let data = reqData[key]
if (key == "path") {
data = "&hellip;/"+data.split(/[\/\\]/).pop()
}
table.appendChild(htmlToElement(`<tr><th style="text-align: right;opacity:0.7;">${label}:</th><td>${data}</td></tr>`))
}
}
}
function galleryImage(reqData) {
let div = document.createElement("div")
div.classList.add("gallery-image")
let img = createElement("img", { style: "cursor: zoom-in;", src: "/image/" + reqData.path}, undefined, undefined)
img.dataset["request"] = JSON.stringify(reqData)
img.addEventListener("click", function() {
function previousImage(img) {
const allImages = Array.from(galleryImageContainer.querySelectorAll(".gallery-image img"))
const index = allImages.indexOf(img)
return allImages.slice(0, index).reverse()[0]
}
function nextImage(img) {
const allImages = Array.from(galleryImageContainer.querySelectorAll(".gallery-image 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(img))
})
let hover = document.createElement("div")
hover.classList.add("gallery-image-hover")
let infoBtn = document.createElement("button")
infoBtn.classList.add("tertiaryButton")
infoBtn.innerHTML = '<i class="fa-regular fa-file-lines"></i>'
infoBtn.addEventListener("click", function() {
let table = document.createElement("table")
galleryDetailTable(table, reqData)
galleryImginfoDialogContent.replaceChildren(table)
galleryImginfoDialog.showModal()
})
hover.appendChild(infoBtn)
let openInNewTabBtn = createElement("button", {style: "margin-left: 0.2em;"}, ["tertiaryButton"],
htmlToElement('<i class="fa-solid fa-arrow-up-right-from-square"></i>'))
openInNewTabBtn.addEventListener("click", (e) => {
openImageInNewTab(img, reqData)
})
hover.appendChild(openInNewTabBtn)
hover.appendChild(document.createElement("br"))
let useAsInputBtn = createElement("button", {}, ["tertiaryButton"], "Use as Input")
useAsInputBtn.addEventListener("click", (e) => {
onUseURLasInput(img.src)
showToast("Loaded image as input image")
})
let useSettingsBtn = createElement("button", {}, ["tertiaryButton"], "Use Settings")
useSettingsBtn.addEventListener("click", (e) => {
restoreTaskToUI({
batchCount: 1,
numOutputsTotal: 1,
reqBody: reqData
})
showToast("Loaded settings")
})
hover.appendChild(useAsInputBtn)
hover.appendChild(document.createElement("br"))
hover.appendChild(useSettingsBtn)
div.replaceChildren(img, hover)
return div
}
function onUseURLasInput(url) {
toDataURL(url, blob => {
onUseAsInputClick(null, {src:blob})
})
}
modalDialogCloseOnBackdropClick(galleryImginfoDialog)
makeDialogDraggable(galleryImginfoDialog)
galleryImginfoDialog.querySelector("#gallery-imginfo-close-button").addEventListener("click", () => {
galleryImginfoDialog.close()
})
galleryThumbnailSize.addEventListener("input", layoutGallery)
window.addEventListener("resize", layoutGallery)
function layoutGallery() {
let thumbSize = parseFloat(galleryThumbnailSize.value)
thumbSize = (10*thumbSize*thumbSize)>>>0
let root = document.querySelector(':root')
root.style.setProperty('--gallery-width', thumbSize + "px")
let msnry = new Masonry(galleryImageContainer, {
gutter: 10,
itemSelector: '.gallery-image',
columnWidth: thumbSize,
fitWidth: true,
})
}
galleryModelSearchField.addEventListener("keyup", debounce(e => refreshGallery(true), 500))
galleryPromptSearchField.addEventListener("keyup", debounce(e => refreshGallery(true), 500))
galleryPageField.addEventListener("keyup", e => {
if (e.code === "Enter") {
e.preventDefault()
refreshGallery(false)
}
})
function refreshGallery(newsearch = false) {
if (newsearch) {
galleryPageField.value = 0
}
galleryImageContainer.innerHTML = ""
let params = new URLSearchParams({
workspace: GALLERY_NAME,
prompt: galleryPromptSearchField.value,
model: galleryModelSearchField.value,
page: galleryPageField.value
})
fetch('/all_images?' + params)
.then(response => response.json())
.then(json => {
if (galleryPageField.value > 0 && json.length == 0) {
decrementGalleryPage()
alert("No more images")
return
}
json.forEach(item => {
galleryImageContainer.appendChild(galleryImage(item))
})
// Wait for all images to be loaded
Promise.all(Array.from(galleryImageContainer.querySelectorAll("img")).map(img => {
if (img.complete)
{
return Promise.resolve(img.naturalHeight !== 0)
}
return new Promise(resolve => {
img.addEventListener('load', () => resolve(true))
img.addEventListener('error', () => resolve(false))
})
})).then(results => {
// then layout the images
layoutGallery()
})
})
params.set("images_per_page", 1)
// 50 has to be replaced if custom images per page is implemented
params.set("page", (parseInt(galleryPageField.value) + 1) * 50)
fetch("/all_images?" + params)
.then(response => response.json())
.then(json => {
if (json.length == 0) {
galleryNextBtn.disabled = true
} else {
galleryNextBtn.disabled = false
}
})
if (galleryPageField.value == 0) {
galleryPrevBtn.disabled = true
} else {
galleryPrevBtn.disabled = false
}
document.getElementById("gallery-refresh").innerText = "Refresh"
}
galleryPrevBtn.addEventListener("click", decrementGalleryPage)
galleryNextBtn.addEventListener("click", incrementGalleryPage)
document.addEventListener("tabClick", (e) => {
if (e.detail.name == 'gallery') {
refreshGallery()
}
})
function decrementGalleryPage() {
let page = Math.max(galleryPageField.value - 1, 0)
galleryPageField.value = page
refreshGallery(false)
}
function incrementGalleryPage() {
galleryPageField.value++
refreshGallery(false)
}

2504
ui/media/js/masonry.pkgd.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -108,6 +108,13 @@ var PARAMETERS = [
return `<input id="${parameter.id}" name="${parameter.id}" size="30">`
},
},
{
id: "use_gallery",
type: ParameterType.checkbox,
label: "Save images to the gallery",
note: "Stores metadata of all images into a database so that they show up on the gallery tab.",
default: false,
},
{
id: "block_nsfw",
type: ParameterType.checkbox,
@ -441,6 +448,7 @@ let testDiffusers = document.querySelector("#use_v3_engine")
let backendEngine = document.querySelector("#backend")
let profileNameField = document.querySelector("#profileName")
let modelsDirField = document.querySelector("#models_dir")
let useGalleryField = document.querySelector("#use_gallery")
let saveSettingsBtn = document.querySelector("#save-system-settings-btn")
@ -593,6 +601,7 @@ function applySettingsFromConfig(config) {
saveToDiskField.addEventListener("change", function(e) {
diskPathField.disabled = !this.checked
metadataOutputFormatField.disabled = !this.checked
useGalleryField.disabled = !this.checked
})
function getCurrentRenderDeviceSelection() {

View File

@ -872,19 +872,19 @@ function createTab(request) {
/* TOAST NOTIFICATIONS */
function showToast(message, duration = 5000, error = false) {
const toast = document.createElement("div")
function showToast(message, duration = 5000, error = false, doc=document) {
const toast = doc.createElement("div")
toast.classList.add("toast-notification")
if (error === true) {
toast.classList.add("toast-notification-error")
}
toast.innerHTML = message
document.body.appendChild(toast)
doc.body.appendChild(toast)
// Set the position of the toast on the screen
const toastCount = document.querySelectorAll(".toast-notification").length
const toastCount = doc.querySelectorAll(".toast-notification").length
const toastHeight = toast.offsetHeight
const previousToastsHeight = Array.from(document.querySelectorAll(".toast-notification"))
const previousToastsHeight = Array.from(doc.querySelectorAll(".toast-notification"))
.slice(0, -1) // exclude current toast
.reduce((totalHeight, toast) => totalHeight + toast.offsetHeight + 10, 0) // add 10 pixels for spacing
toast.style.bottom = `${10 + previousToastsHeight}px`
@ -896,7 +896,7 @@ function showToast(message, duration = 5000, error = false) {
const removeTimeoutId = setTimeout(() => {
toast.remove()
// Adjust the position of remaining toasts
const remainingToasts = document.querySelectorAll(".toast-notification")
const remainingToasts = doc.querySelectorAll(".toast-notification")
const removedToastBottom = toast.getBoundingClientRect().bottom
remainingToasts.forEach((toast) => {
@ -908,13 +908,13 @@ function showToast(message, duration = 5000, error = false) {
// Wait for the slide-down animation to complete
setTimeout(() => {
// Remove the slide-down class after the animation has completed
const slidingToasts = document.querySelectorAll(".slide-down")
const slidingToasts = doc.querySelectorAll(".slide-down")
slidingToasts.forEach((toast) => {
toast.classList.remove("slide-down")
})
// Adjust the position of remaining toasts again, in case there are multiple toasts being removed at once
const remainingToastsDown = document.querySelectorAll(".toast-notification")
const remainingToastsDown = doc.querySelectorAll(".toast-notification")
let heightSoFar = 0
remainingToastsDown.forEach((toast) => {
toast.style.bottom = `${10 + heightSoFar}px`
@ -1232,3 +1232,18 @@ function playSound() {
})
}
}
function toDataURL(url, callback){
var xhr = new XMLHttpRequest()
xhr.open('get', url)
xhr.responseType = 'blob'
xhr.onload = function(){
var fr = new FileReader()
fr.onload = function(){
callback(this.result)
}
fr.readAsDataURL(xhr.response)
}
xhr.send()
}