Thumbnails for Embeddings (#1483)

* sqlalchemy

* sqlalchemy

* sqlalchemy bucket v1

* Bucket API

* Move easydb files to its own folders

* show images

* add croppr

* croppr ui

* profile, thumbnail croppr size limit

* fill list

* add upload

* Use modifiers card, resize thumbs

* Remove debugging code

* remove unused variable
This commit is contained in:
JeLuF 2023-08-17 08:03:05 +02:00 committed by GitHub
parent 285792f692
commit 7270b5fe0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1834 additions and 21 deletions

View File

@ -712,3 +712,31 @@ FileSaver.js is licensed under the MIT license:
SOFTWARE. SOFTWARE.
[1]: http://eligrey.com [1]: http://eligrey.com
croppr.js
=========
https://github.com/jamesssooi/Croppr.js
croppr.js is licensed under the MIT license:
MIT License
Copyright (c) 2017 James Ooi
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

@ -25,6 +25,8 @@ modules_to_check = {
"fastapi": "0.85.1", "fastapi": "0.85.1",
"pycloudflared": "0.2.0", "pycloudflared": "0.2.0",
"ruamel.yaml": "0.17.21", "ruamel.yaml": "0.17.21",
"sqlalchemy": "2.0.19",
"python-multipart": "0.0.6",
# "xformers": "0.0.16", # "xformers": "0.0.16",
} }
modules_to_log = ["torch", "torchvision", "sdkit", "stable-diffusion-sdkit"] modules_to_log = ["torch", "torchvision", "sdkit", "stable-diffusion-sdkit"]

View File

@ -38,6 +38,7 @@ SD_UI_DIR = os.getenv("SD_UI_PATH", None)
CONFIG_DIR = os.path.abspath(os.path.join(SD_UI_DIR, "..", "scripts")) CONFIG_DIR = os.path.abspath(os.path.join(SD_UI_DIR, "..", "scripts"))
MODELS_DIR = os.path.abspath(os.path.join(SD_DIR, "..", "models")) MODELS_DIR = os.path.abspath(os.path.join(SD_DIR, "..", "models"))
BUCKET_DIR = os.path.abspath(os.path.join(SD_DIR, "..", "bucket"))
USER_PLUGINS_DIR = os.path.abspath(os.path.join(SD_DIR, "..", "plugins")) USER_PLUGINS_DIR = os.path.abspath(os.path.join(SD_DIR, "..", "plugins"))
CORE_PLUGINS_DIR = os.path.abspath(os.path.join(SD_UI_DIR, "plugins")) CORE_PLUGINS_DIR = os.path.abspath(os.path.join(SD_UI_DIR, "plugins"))

View File

@ -0,0 +1,102 @@
from typing import List
from fastapi import Depends, FastAPI, HTTPException, Response, File
from sqlalchemy.orm import Session
from easydiffusion.easydb import crud, models, schemas
from easydiffusion.easydb.database import SessionLocal, engine
from requests.compat import urlparse
import base64, json
MIME_TYPES = {
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"gif": "image/gif",
"png": "image/png",
"webp": "image/webp",
"js": "text/javascript",
"htm": "text/html",
"html": "text/html",
"css": "text/css",
"json": "application/json",
"mjs": "application/json",
"yaml": "application/yaml",
"svg": "image/svg+xml",
"txt": "text/plain",
}
def init():
from easydiffusion.server import server_api
models.BucketBase.metadata.create_all(bind=engine)
# Dependency
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@server_api.get("/bucket/{obj_path:path}")
def bucket_get_object(obj_path: str, db: Session = Depends(get_db)):
filename = get_filename_from_url(obj_path)
path = get_path_from_url(obj_path)
if filename==None:
bucket = crud.get_bucket_by_path(db, path=path)
if bucket == None:
raise HTTPException(status_code=404, detail="Bucket not found")
bucketfiles = db.query(models.BucketFile).with_entities(models.BucketFile.filename).filter(models.BucketFile.bucket_id == bucket.id).all()
bucketfiles = [ x.filename for x in bucketfiles ]
return bucketfiles
else:
bucket_id = crud.get_bucket_by_path(db, path).id
bucketfile = db.query(models.BucketFile).filter(models.BucketFile.bucket_id == bucket_id, models.BucketFile.filename == filename).first()
suffix = get_suffix_from_filename(filename)
return Response(content=bucketfile.data, media_type=MIME_TYPES.get(suffix, "application/octet-stream"))
@server_api.post("/bucket/{obj_path:path}")
def bucket_post_object(obj_path: str, file: bytes = File(), db: Session = Depends(get_db)):
filename = get_filename_from_url(obj_path)
path = get_path_from_url(obj_path)
bucket = crud.get_bucket_by_path(db, path)
if bucket == None:
bucket = crud.create_bucket(db=db, bucket=schemas.BucketCreate(path=path))
bucket_id = bucket.id
bucketfile = schemas.BucketFileCreate(filename=filename, data=file)
result = crud.create_bucketfile(db=db, bucketfile=bucketfile, bucket_id=bucket_id)
result.data = base64.encodestring(result.data)
return result
@server_api.post("/buckets/{bucket_id}/items/", response_model=schemas.BucketFile)
def create_bucketfile_in_bucket(
bucket_id: int, bucketfile: schemas.BucketFileCreate, db: Session = Depends(get_db)
):
bucketfile.data = base64.decodestring(bucketfile.data)
result = crud.create_bucketfile(db=db, bucketfile=bucketfile, bucket_id=bucket_id)
result.data = base64.encodestring(result.data)
return result
def get_filename_from_url(url):
path = urlparse(url).path
name = path[path.rfind('/')+1:]
return name or None
def get_path_from_url(url):
path = urlparse(url).path
path = path[0:path.rfind('/')]
return path or None
def get_suffix_from_filename(filename):
return filename[filename.rfind('.')+1:]

View File

@ -0,0 +1,24 @@
from sqlalchemy.orm import Session
from easydiffusion.easydb import models, schemas
def get_bucket_by_path(db: Session, path: str):
return db.query(models.Bucket).filter(models.Bucket.path == path).first()
def create_bucket(db: Session, bucket: schemas.BucketCreate):
db_bucket = models.Bucket(path=bucket.path)
db.add(db_bucket)
db.commit()
db.refresh(db_bucket)
return db_bucket
def create_bucketfile(db: Session, bucketfile: schemas.BucketFileCreate, bucket_id: int):
db_bucketfile = models.BucketFile(**bucketfile.dict(), bucket_id=bucket_id)
db.merge(db_bucketfile)
db.commit()
db_bucketfile = db.query(models.BucketFile).filter(models.BucketFile.bucket_id==bucket_id, models.BucketFile.filename==bucketfile.filename).first()
return db_bucketfile

View File

@ -0,0 +1,14 @@
import os
from easydiffusion import app
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
os.makedirs(app.BUCKET_DIR, exist_ok=True)
SQLALCHEMY_DATABASE_URL = "sqlite:///"+os.path.join(app.BUCKET_DIR, "bucket.db")
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
BucketBase = declarative_base()

View File

@ -0,0 +1,25 @@
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, BLOB
from sqlalchemy.orm import relationship
from easydiffusion.easydb.database import BucketBase
class Bucket(BucketBase):
__tablename__ = "bucket"
id = Column(Integer, primary_key=True, index=True)
path = Column(String, unique=True, index=True)
bucketfiles = relationship("BucketFile", back_populates="bucket")
class BucketFile(BucketBase):
__tablename__ = "bucketfile"
filename = Column(String, index=True, primary_key=True)
bucket_id = Column(Integer, ForeignKey("bucket.id"), primary_key=True)
data = Column(BLOB, index=False)
bucket = relationship("Bucket", back_populates="bucketfiles")

View File

@ -0,0 +1,36 @@
from typing import List, Union
from pydantic import BaseModel
class BucketFileBase(BaseModel):
filename: str
data: bytes
class BucketFileCreate(BucketFileBase):
pass
class BucketFile(BucketFileBase):
bucket_id: int
class Config:
orm_mode = True
class BucketBase(BaseModel):
path: str
class BucketCreate(BucketBase):
pass
class Bucket(BucketBase):
id: int
bucketfiles: List[BucketFile] = []
class Config:
orm_mode = True

View File

@ -18,12 +18,14 @@
<link rel="stylesheet" href="/media/css/image-modal.css"> <link rel="stylesheet" href="/media/css/image-modal.css">
<link rel="stylesheet" href="/media/css/plugins.css"> <link rel="stylesheet" href="/media/css/plugins.css">
<link rel="stylesheet" href="/media/css/animations.css"> <link rel="stylesheet" href="/media/css/animations.css">
<link rel="stylesheet" href="/media/css/croppr.css" rel="stylesheet"/>
<link rel="manifest" href="/media/manifest.webmanifest"> <link rel="manifest" href="/media/manifest.webmanifest">
<script src="/media/js/jquery-3.6.1.min.js"></script> <script src="/media/js/jquery-3.6.1.min.js"></script>
<script src="/media/js/jquery-confirm.min.js"></script> <script src="/media/js/jquery-confirm.min.js"></script>
<script src="/media/js/jszip.min.js"></script> <script src="/media/js/jszip.min.js"></script>
<script src="/media/js/FileSaver.min.js"></script> <script src="/media/js/FileSaver.min.js"></script>
<script src="/media/js/marked.min.js"></script> <script src="/media/js/marked.min.js"></script>
<script src="/media/js/croppr.js"></script>
</head> </head>
<body> <body>
<div id="container"> <div id="container">
@ -689,6 +691,15 @@
</button> </button>
<i class="fa-solid fa-magnifying-glass"></i> <i class="fa-solid fa-magnifying-glass"></i>
<input id="embeddings-search-box" type="text" spellcheck="false" autocomplete="off" placeholder="Search..."> <input id="embeddings-search-box" type="text" spellcheck="false" autocomplete="off" placeholder="Search...">
<label for="embedding-card-size-selector"><small>Thumbnail Size:</small></label>
<select id="embedding-card-size-selector" name="embedding-card-size-selector">
<option value="-2">0</option>
<option value="-1">1</option>
<option value="0">2</option>
<option value="1" selected>3</option>
<option value="2">4</option>
<option value="3">5</option>
</select>
<span style="float:right;"><label>Mode:</label>&nbsp;<select id="embeddings-mode"><option value="insert">Insert at cursor position</option><option value="append">Append at the end</option></select> <span style="float:right;"><label>Mode:</label>&nbsp;<select id="embeddings-mode"><option value="insert">Insert at cursor position</option><option value="append">Append at the end</option></select>
</div> </div>
<div id="embeddings-list"> <div id="embeddings-list">
@ -696,6 +707,34 @@
</div> </div>
</dialog> </dialog>
<dialog id="use-as-thumb-dialog">
<div id="use-as-thumb-dialog-header" class="dialog-header">
<div id="use-as-thumb-dialog-header-left" class="dialog-header-left">
<h4>Use as thumbnail</h4>
<span>Use a pictures as thumbnail for embeddings, LORAs, etc.</span>
</div>
<div id="use-as-thumb-dialog-header-right">
<i id="use-as-thumb-dialog-close-button" class="fa-solid fa-xmark fa-lg"></i>
</div>
</div>
<div>
<div class="use-as-thumb-grid">
<div class="use-as-thumb-preview">
<div id="use-as-thumb-img-container"><img id="use-as-thumb-image" src="/media/images/noimg.png" width="512" height="512"></div>
</div>
<div class="use-as-thumb-select">
<label for="use-as-thumb-select">Use the thumbnail for:</label><br>
<select id="use-as-thumb-select" size="16" multiple>
</select>
</div>
<div class="use-as-thumb-buttons">
<button class="tertiaryButton" id="use-as-thumb-save">Save thumbnail</button>
<button class="tertiaryButton" id="use-as-thumb-cancel">Cancel</button>
</div>
</div>
</div>
</dialog>
<div id="image-editor" class="popup image-editor-popup"> <div id="image-editor" class="popup image-editor-popup">
<div> <div>
<i class="close-button fa-solid fa-xmark"></i> <i class="close-button fa-solid fa-xmark"></i>

View File

@ -1,4 +1,4 @@
from easydiffusion import model_manager, app, server from easydiffusion import model_manager, app, server, bucket_manager
from easydiffusion.server import server_api # required for uvicorn from easydiffusion.server import server_api # required for uvicorn
app.init() app.init()
@ -8,6 +8,7 @@ server.init()
# Init the app # Init the app
model_manager.init() model_manager.init()
app.init_render_threads() app.init_render_threads()
bucket_manager.init()
# start the browser ui # start the browser ui
app.open_browser() app.open_browser()

58
ui/media/css/croppr.css Normal file
View File

@ -0,0 +1,58 @@
.croppr-container * {
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
box-sizing: border-box;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
}
.croppr-container img {
vertical-align: middle;
max-width: 100%;
}
.croppr {
position: relative;
display: inline-block;
}
.croppr-overlay {
background: rgba(0,0,0,0.5);
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
cursor: crosshair;
}
.croppr-region {
border: 1px dashed rgba(0, 0, 0, 0.5);
position: absolute;
z-index: 3;
cursor: move;
top: 0;
}
.croppr-imageClipped {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
pointer-events: none;
}
.croppr-handle {
border: 1px solid black;
background-color: white;
width: 10px;
height: 10px;
position: absolute;
z-index: 4;
top: 0;
}

View File

@ -1662,6 +1662,35 @@ body.wait-pause {
} }
} }
.spinner-container {
width: 80px;
height: 100px;
margin: 100px auto;
margin-top: 30vH;
}
.spinner-block {
position: relative;
box-sizing: border-box;
float: left;
margin: 0 10px 10px 0;
width: 12px;
height: 12px;
border-radius: 3px;
background: var(--accent-color);
}
.spinner-block:nth-child(4n+1) { animation: spinner-wave 2s ease .0s infinite; }
.spinner-block:nth-child(4n+2) { animation: spinner-wave 2s ease .2s infinite; }
.spinner-block:nth-child(4n+3) { animation: spinner-wave 2s ease .4s infinite; }
.spinner-block:nth-child(4n+4) { animation: spinner-wave 2s ease .6s infinite; margin-right: 0; }
@keyframes spinner-wave {
0% { top: 0; opacity: 1; }
50% { top: 30px; opacity: .2; }
100% { top: 0; opacity: 1; }
}
#embeddings-dialog { #embeddings-dialog {
overflow: clip; overflow: clip;
} }
@ -1753,6 +1782,32 @@ body.wait-pause {
float: right; float: right;
} }
.use-as-thumb-grid { display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: 1fr auto;
gap: 8px 8px;
grid-auto-flow: row;
grid-template-areas:
"uat-preview uat-select"
"uat-preview uat-buttons";
}
.use-as-thumb-preview {
justify-self: center;
align-self: center;
grid-area: uat-preview;
}
.use-as-thumb-select {
grid-area: uat-select;
}
.use-as-thumb-buttons {
justify-self: center;
grid-area: uat-buttons;
}
.diffusers-disabled-on-startup .diffusers-restart-needed { .diffusers-disabled-on-startup .diffusers-restart-needed {
font-size: 0; font-size: 0;
} }

BIN
ui/media/images/noimg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -45,6 +45,7 @@ const SETTINGS_IDS_LIST = [
"sound_toggle", "sound_toggle",
"vram_usage_level", "vram_usage_level",
"confirm_dangerous_actions", "confirm_dangerous_actions",
"profileName",
"metadata_output_format", "metadata_output_format",
"auto_save_settings", "auto_save_settings",
"apply_color_correction", "apply_color_correction",
@ -55,6 +56,7 @@ const SETTINGS_IDS_LIST = [
"tree_toggle", "tree_toggle",
"json_toggle", "json_toggle",
"extract_lora_from_prompt", "extract_lora_from_prompt",
"embedding-card-size-selector",
] ]
const IGNORE_BY_DEFAULT = ["prompt"] const IGNORE_BY_DEFAULT = ["prompt"]

1189
ui/media/js/croppr.js Executable file

File diff suppressed because it is too large Load Diff

View File

@ -141,6 +141,7 @@ let embeddingsDialogCloseBtn = embeddingsDialog.querySelector("#embeddings-dialo
let embeddingsSearchBox = document.querySelector("#embeddings-search-box") let embeddingsSearchBox = document.querySelector("#embeddings-search-box")
let embeddingsList = document.querySelector("#embeddings-list") let embeddingsList = document.querySelector("#embeddings-list")
let embeddingsModeField = document.querySelector("#embeddings-mode") let embeddingsModeField = document.querySelector("#embeddings-mode")
let embeddingsCardSizeSelector = document.querySelector("#embedding-card-size-selector")
let positiveEmbeddingText = document.querySelector("#positive-embedding-text") let positiveEmbeddingText = document.querySelector("#positive-embedding-text")
let negativeEmbeddingText = document.querySelector("#negative-embedding-text") let negativeEmbeddingText = document.querySelector("#negative-embedding-text")
@ -170,6 +171,12 @@ let saveAllTreeToggle = document.querySelector("#tree_toggle")
let saveAllJSONToggle = document.querySelector("#json_toggle") let saveAllJSONToggle = document.querySelector("#json_toggle")
let saveAllFoldersOption = document.querySelector("#download-add-folders") let saveAllFoldersOption = document.querySelector("#download-add-folders")
let splashScreenPopup = document.querySelector("#splash-screen") let splashScreenPopup = document.querySelector("#splash-screen")
let useAsThumbDialog = document.querySelector("#use-as-thumb-dialog")
let useAsThumbDialogCloseBtn = document.querySelector("#use-as-thumb-dialog-close-button")
let useAsThumbImageContainer = document.querySelector("#use-as-thumb-img-container")
let useAsThumbSelect = document.querySelector("#use-as-thumb-select")
let useAsThumbSaveBtn = document.querySelector("#use-as-thumb-save")
let useAsThumbCancelBtn = document.querySelector("#use-as-thumb-cancel")
let maskSetting = document.querySelector("#enable_mask") let maskSetting = document.querySelector("#enable_mask")
@ -538,6 +545,7 @@ function showImages(reqBody, res, outputContainer, livePreview) {
{ text: "Upscale", on_click: onUpscaleClick }, { text: "Upscale", on_click: onUpscaleClick },
{ text: "Fix Faces", on_click: onFixFacesClick }, { text: "Fix Faces", on_click: onFixFacesClick },
], ],
{ text: "Use as Thumbnail", on_click: onUseAsThumbnailClick },
] ]
// include the plugins // include the plugins
@ -682,6 +690,127 @@ function onMakeSimilarClick(req, img) {
createTask(newTaskRequest) createTask(newTaskRequest)
} }
// gets a flat list of all models of a certain type, ignoring directories
function getAllModelNames(type) {
function f(tree) {
if (tree == undefined) {
return []
}
let result=[];
tree.forEach( e => {
if (typeof(e) == "object") {
result = result.concat( f(e[1]))
} else {
result.push(e)
}
});
return result
}
return f(modelsOptions[type])
}
function onUseAsThumbnailClick(req, img) {
let scale = 1
let targetWidth = img.naturalWidth
let targetHeight = img.naturalHeight
let resize = false
onUseAsThumbnailClick.img = img
if ( typeof(onUseAsThumbnailClick.croppr) == 'undefined' ) {
onUseAsThumbnailClick.croppr = new Croppr("#use-as-thumb-image", { aspectRatio: 1, minSize: [384,384,'px'], startSize: [512, 512, 'px'], returnMode:"real" })
}
if (img.naturalWidth > img.naturalHeight) {
if (img.naturalWidth > 768) {
scale = 768 / img.naturalWidth
targetWidth = 768
targetHeight = (img.naturalHeight*scale)>>>0
resize = true
}
} else {
if (img.naturalHeight > 768) {
scale = 768 / img.naturalHeight
targetHeight = 768
targetWidth = (img.naturalWidth*scale)>>>0
resize = true
}
}
onUseAsThumbnailClick.croppr.options.minSize = {width: 384*scale>>>0, height: 384*scale>>>0}
onUseAsThumbnailClick.croppr.options.startSize = {width: 512*scale>>>0, height: 512*scale>>>0}
if (resize) {
const canvas = document.createElement('canvas')
canvas.width = targetWidth
canvas.height = targetHeight
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0, targetWidth, targetHeight)
onUseAsThumbnailClick.croppr.setImage(canvas.toDataURL('image/png'))
} else {
onUseAsThumbnailClick.croppr.setImage(img.src)
}
let embeddings = getAllModelNames("embeddings").filter( e => req.prompt.includes(e) || req.negative_prompt.includes(e) )
let LORA = []
if ("use_lora_model" in req) {
LORA=req.use_lora_model
}
let optgroup = document.createElement("optgroup")
optgroup.label = "Embeddings"
optgroup.replaceChildren(...embeddings.map(e => {
let option = document.createElement("option")
option.innerText = e
option.dataset["type"] = "embeddings"
return option
}))
useAsThumbSelect.replaceChildren(optgroup)
useAsThumbDialog.showModal()
onUseAsThumbnailClick.scale = scale
}
modalDialogCloseOnBackdropClick(useAsThumbDialog)
makeDialogDraggable(useAsThumbDialog)
useAsThumbDialogCloseBtn.addEventListener("click", () => {
useAsThumbDialog.close()
})
useAsThumbCancelBtn.addEventListener("click", () => {
useAsThumbDialog.close()
})
useAsThumbSaveBtn.addEventListener("click", (e) => {
let scale = 1/onUseAsThumbnailClick.scale
let crop = onUseAsThumbnailClick.croppr.getValue()
let len = Math.max(crop.width*scale, 384)
let profileName = profileNameField.value
cropImageDataUrl(onUseAsThumbnailClick.img.src, crop.x*scale, crop.y*scale, len, len)
.then(thumb => fetch(thumb))
.then(response => response.blob())
.then(async function(blob) {
const formData = new FormData()
formData.append("file", blob)
let options = useAsThumbSelect.selectedOptions
let promises = []
for (let embedding of options) {
promises.push(fetch(`bucket/${profileName}/${embedding.dataset["type"]}/${embedding.value}.png`, { method: 'POST', body: formData }))
}
return Promise.all(promises)
}).then(() => {
useAsThumbDialog.close()
})
.catch(error => {
console.error(error)
showToast("Couldn't save thumbnail.<br>"+error)
})
})
function enqueueImageVariationTask(req, img, reqDiff) { function enqueueImageVariationTask(req, img, reqDiff) {
const imageSeed = img.getAttribute("data-seed") const imageSeed = img.getAttribute("data-seed")
@ -2594,31 +2723,52 @@ document.getElementById("toggle-tensorrt-install").addEventListener("click", fun
/* Embeddings */ /* Embeddings */
function updateEmbeddingsList(filter = "") { function updateEmbeddingsList(filter = "") {
function html(model, prefix = "", filter = "") { function html(model, iconlist = [], prefix = "", filter = "") {
filter = filter.toLowerCase() filter = filter.toLowerCase()
let toplevel = "" let toplevel = document.createElement("div")
let folders = "" let folders = document.createElement("div")
let embIcon = Object.assign({}, ...iconlist.map( x=> ({[x.toLowerCase().split('.').slice(0,-1).join('.')]:x})))
let profileName = profileNameField.value
model?.forEach((m) => { model?.forEach((m) => {
if (typeof m == "string") { if (typeof m == "string") {
if (m.toLowerCase().search(filter) != -1) { let token=m.toLowerCase()
toplevel += `<button data-embedding="${m}">${m}</button> ` if (token.search(filter) != -1) {
let button
if (iconlist.length==0) {
button = document.createElement("button")
button.innerText="m"
} else {
let img = '/media/images/noimg.png'
if (token in embIcon) {
img = `/bucket/${profileName}/embeddings/${embIcon[token]}`
}
button = createModifierCard(m, [img,img], true)
}
button.dataset["embedding"] = m
button.addEventListener("click", onButtonClick)
toplevel.appendChild(button)
} }
} else { } else {
let subdir = html(m[1], prefix + m[0] + "/", filter) let subdir = html(m[1], iconlist, prefix + m[0] + "/", filter)
if (subdir != "") { if (typeof(subdir) == "object") {
folders += let div1 = document.createElement("div")
`<div class="embedding-category"><h4 class="collapsible">${prefix}${m[0]}</h4><div class="collapsible-content">` + let div2 = document.createElement("div")
subdir + div1.classList.add("collapsible-content")
"</div></div>" div1.classList.add("embedding-category")
div1.appendChild(subdir)
div2.replaceChildren(htmlToElement(`<h4 class="collapsible">${prefix}${m[0]}</h4>`), div1)
folders.appendChild(div2)
} }
} }
}) })
return toplevel + folders let result = document.createElement("div")
result.replaceChildren(toplevel, htmlToElement('<br style="clear: both;">'), folders)
return result
} }
function onButtonClick(e) { function onButtonClick(e) {
let text = e.target.dataset["embedding"] let text = e.target.closest("[data-embedding]").dataset["embedding"]
const insertIntoNegative = e.shiftKey || positiveEmbeddingText.classList.contains("displayNone") const insertIntoNegative = e.shiftKey || positiveEmbeddingText.classList.contains("displayNone")
if (embeddingsModeField.value == "insert") { if (embeddingsModeField.value == "insert") {
@ -2643,14 +2793,38 @@ function updateEmbeddingsList(filter = "") {
} }
} }
embeddingsList.innerHTML = html(modelsOptions.embeddings, "", filter) // Usually the rendering of the Embeddings HTML takes less than a second. In case it takes longer, show a spinner
embeddingsList.querySelectorAll("button").forEach((b) => { embeddingsList.innerHTML = `
b.addEventListener("click", onButtonClick) <div class="spinner-container">
}) <div class="spinner-block"></div> <div class="spinner-block"></div> <div class="spinner-block"></div> <div class="spinner-block"></div>
<div class="spinner-block"></div> <div class="spinner-block"></div> <div class="spinner-block"></div> <div class="spinner-block"></div>
<div class="spinner-block"></div> <div class="spinner-block"></div> <div class="spinner-block"></div> <div class="spinner-block"></div>
<div class="spinner-block"></div> <div class="spinner-block"></div> <div class="spinner-block"></div> <div class="spinner-block"></div>
</div>
`
// Remove after fixing https://github.com/huggingface/diffusers/issues/3922
let warning = "<div></div>"
if (vramUsageLevelField.value == "low") {
warning = `
<div style="border-color: var(--accent-color); border-width: 4px; border-radius: 1em; border-style: solid; background: black; text-align: center; padding: 1em; margin: 1em; ">
<i class="fa fa-fire" style="color:#f7630c;"></i> Warning: Your GPU memory profile is set to "Low". Embeddings currently only work in "Balanced" mode!
</div>`
}
// END of remove block
let profileName = profileNameField.value
fetch(`/bucket/${profileName}/embeddings/`)
.then(response => response.status==200 ? response.json(): [])
.then(async function(iconlist) {
embeddingsList.replaceChildren(htmlToElement(warning), html(modelsOptions.embeddings, iconlist, "", filter))
createCollapsibles(embeddingsList) createCollapsibles(embeddingsList)
if (filter != "") { if (filter != "") {
embeddingsExpandAll() embeddingsExpandAll()
} }
resizeModifierCards(embeddingsCardSizeSelector.value)
})
} }
function showEmbeddingDialog() { function showEmbeddingDialog() {
@ -2658,23 +2832,33 @@ function showEmbeddingDialog() {
embeddingsSearchBox.value = "" embeddingsSearchBox.value = ""
embeddingsDialog.showModal() embeddingsDialog.showModal()
} }
embeddingsButton.addEventListener("click", () => { embeddingsButton.addEventListener("click", () => {
positiveEmbeddingText.classList.remove("displayNone") positiveEmbeddingText.classList.remove("displayNone")
negativeEmbeddingText.classList.add("displayNone") negativeEmbeddingText.classList.add("displayNone")
showEmbeddingDialog() showEmbeddingDialog()
}) })
negativeEmbeddingsButton.addEventListener("click", () => { negativeEmbeddingsButton.addEventListener("click", () => {
positiveEmbeddingText.classList.add("displayNone") positiveEmbeddingText.classList.add("displayNone")
negativeEmbeddingText.classList.remove("displayNone") negativeEmbeddingText.classList.remove("displayNone")
showEmbeddingDialog() showEmbeddingDialog()
}) })
embeddingsDialogCloseBtn.addEventListener("click", (e) => { embeddingsDialogCloseBtn.addEventListener("click", (e) => {
embeddingsDialog.close() embeddingsDialog.close()
}) })
embeddingsSearchBox.addEventListener("input", (e) => { embeddingsSearchBox.addEventListener("input", (e) => {
updateEmbeddingsList(embeddingsSearchBox.value) updateEmbeddingsList(embeddingsSearchBox.value)
}) })
embeddingsCardSizeSelector.addEventListener("change", (e) => {
resizeModifierCards(embeddingsCardSizeSelector.value)
})
modalDialogCloseOnBackdropClick(embeddingsDialog) modalDialogCloseOnBackdropClick(embeddingsDialog)
makeDialogDraggable(embeddingsDialog) makeDialogDraggable(embeddingsDialog)

View File

@ -194,6 +194,16 @@ var PARAMETERS = [
icon: "fa-check-double", icon: "fa-check-double",
default: true, default: true,
}, },
{
id: "profileName",
type: ParameterType.custom,
label: "Profile Name",
note: "Name of the profile for model manager settings, e.g. thumbnails for embeddings. Use this to have different settings for different users.",
render: (parameter) => {
return `<input id="${parameter.id}" name="${parameter.id}" value="default" size="12">`
},
icon: "fa-user-gear",
},
{ {
id: "listen_to_network", id: "listen_to_network",
type: ParameterType.checkbox, type: ParameterType.checkbox,
@ -410,6 +420,7 @@ let useBetaChannelField = document.querySelector("#use_beta_channel")
let uiOpenBrowserOnStartField = document.querySelector("#ui_open_browser_on_start") let uiOpenBrowserOnStartField = document.querySelector("#ui_open_browser_on_start")
let confirmDangerousActionsField = document.querySelector("#confirm_dangerous_actions") let confirmDangerousActionsField = document.querySelector("#confirm_dangerous_actions")
let testDiffusers = document.querySelector("#test_diffusers") let testDiffusers = document.querySelector("#test_diffusers")
let profileNameField = document.querySelector("#profileName")
let saveSettingsBtn = document.querySelector("#save-system-settings-btn") let saveSettingsBtn = document.querySelector("#save-system-settings-btn")

View File

@ -1097,6 +1097,48 @@ async function deleteKeys(keyToDelete) {
} }
} }
/**
* @param {String} Data URL of the image
* @param {Integer} Top left X-coordinate of the crop area
* @param {Integer} Top left Y-coordinate of the crop area
* @param {Integer} Width of the crop area
* @param {Integer} Height of the crop area
* @return {String}
*/
function cropImageDataUrl(dataUrl, x, y, width, height) {
return new Promise((resolve, reject) => {
const image = new Image()
image.src = dataUrl
image.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
ctx.drawImage(image, x, y, width, height, 0, 0, width, height)
const croppedDataUrl = canvas.toDataURL('image/png')
resolve(croppedDataUrl)
}
image.onerror = (error) => {
reject(error)
}
})
}
/**
* @param {String} HTML representing a single element
* @return {Element}
*/
function htmlToElement(html) {
var template = document.createElement('template');
html = html.trim(); // Never return a text node of whitespace as the result
template.innerHTML = html;
return template.content.firstChild;
}
function modalDialogCloseOnBackdropClick(dialog) { function modalDialogCloseOnBackdropClick(dialog) {
dialog.addEventListener('mousedown', function (event) { dialog.addEventListener('mousedown', function (event) {
// Firefox creates an event with clientX|Y = 0|0 when choosing an <option>. // Firefox creates an event with clientX|Y = 0|0 when choosing an <option>.