forked from extern/easydiffusion
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:
parent
285792f692
commit
7270b5fe0c
@ -712,3 +712,31 @@ FileSaver.js is licensed under the MIT license:
|
||||
SOFTWARE.
|
||||
|
||||
[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.
|
||||
|
@ -25,6 +25,8 @@ modules_to_check = {
|
||||
"fastapi": "0.85.1",
|
||||
"pycloudflared": "0.2.0",
|
||||
"ruamel.yaml": "0.17.21",
|
||||
"sqlalchemy": "2.0.19",
|
||||
"python-multipart": "0.0.6",
|
||||
# "xformers": "0.0.16",
|
||||
}
|
||||
modules_to_log = ["torch", "torchvision", "sdkit", "stable-diffusion-sdkit"]
|
||||
|
@ -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"))
|
||||
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"))
|
||||
CORE_PLUGINS_DIR = os.path.abspath(os.path.join(SD_UI_DIR, "plugins"))
|
||||
|
102
ui/easydiffusion/bucket_manager.py
Normal file
102
ui/easydiffusion/bucket_manager.py
Normal 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:]
|
24
ui/easydiffusion/easydb/crud.py
Normal file
24
ui/easydiffusion/easydb/crud.py
Normal 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
|
||||
|
14
ui/easydiffusion/easydb/database.py
Normal file
14
ui/easydiffusion/easydb/database.py
Normal 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()
|
25
ui/easydiffusion/easydb/models.py
Normal file
25
ui/easydiffusion/easydb/models.py
Normal 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")
|
||||
|
36
ui/easydiffusion/easydb/schemas.py
Normal file
36
ui/easydiffusion/easydb/schemas.py
Normal 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
|
||||
|
@ -18,12 +18,14 @@
|
||||
<link rel="stylesheet" href="/media/css/image-modal.css">
|
||||
<link rel="stylesheet" href="/media/css/plugins.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">
|
||||
<script src="/media/js/jquery-3.6.1.min.js"></script>
|
||||
<script src="/media/js/jquery-confirm.min.js"></script>
|
||||
<script src="/media/js/jszip.min.js"></script>
|
||||
<script src="/media/js/FileSaver.min.js"></script>
|
||||
<script src="/media/js/marked.min.js"></script>
|
||||
<script src="/media/js/croppr.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
@ -689,6 +691,15 @@
|
||||
</button>
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
<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> <select id="embeddings-mode"><option value="insert">Insert at cursor position</option><option value="append">Append at the end</option></select>
|
||||
</div>
|
||||
<div id="embeddings-list">
|
||||
@ -696,6 +707,34 @@
|
||||
</div>
|
||||
</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>
|
||||
<i class="close-button fa-solid fa-xmark"></i>
|
||||
|
@ -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
|
||||
|
||||
app.init()
|
||||
@ -8,6 +8,7 @@ server.init()
|
||||
# Init the app
|
||||
model_manager.init()
|
||||
app.init_render_threads()
|
||||
bucket_manager.init()
|
||||
|
||||
# start the browser ui
|
||||
app.open_browser()
|
||||
|
58
ui/media/css/croppr.css
Normal file
58
ui/media/css/croppr.css
Normal 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;
|
||||
}
|
@ -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 {
|
||||
overflow: clip;
|
||||
}
|
||||
@ -1753,6 +1782,32 @@ body.wait-pause {
|
||||
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 {
|
||||
font-size: 0;
|
||||
}
|
||||
|
BIN
ui/media/images/noimg.png
Normal file
BIN
ui/media/images/noimg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
@ -45,6 +45,7 @@ const SETTINGS_IDS_LIST = [
|
||||
"sound_toggle",
|
||||
"vram_usage_level",
|
||||
"confirm_dangerous_actions",
|
||||
"profileName",
|
||||
"metadata_output_format",
|
||||
"auto_save_settings",
|
||||
"apply_color_correction",
|
||||
@ -55,6 +56,7 @@ const SETTINGS_IDS_LIST = [
|
||||
"tree_toggle",
|
||||
"json_toggle",
|
||||
"extract_lora_from_prompt",
|
||||
"embedding-card-size-selector",
|
||||
]
|
||||
|
||||
const IGNORE_BY_DEFAULT = ["prompt"]
|
||||
|
1189
ui/media/js/croppr.js
Executable file
1189
ui/media/js/croppr.js
Executable file
File diff suppressed because it is too large
Load Diff
@ -141,6 +141,7 @@ let embeddingsDialogCloseBtn = embeddingsDialog.querySelector("#embeddings-dialo
|
||||
let embeddingsSearchBox = document.querySelector("#embeddings-search-box")
|
||||
let embeddingsList = document.querySelector("#embeddings-list")
|
||||
let embeddingsModeField = document.querySelector("#embeddings-mode")
|
||||
let embeddingsCardSizeSelector = document.querySelector("#embedding-card-size-selector")
|
||||
|
||||
let positiveEmbeddingText = document.querySelector("#positive-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 saveAllFoldersOption = document.querySelector("#download-add-folders")
|
||||
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")
|
||||
|
||||
@ -538,6 +545,7 @@ function showImages(reqBody, res, outputContainer, livePreview) {
|
||||
{ text: "Upscale", on_click: onUpscaleClick },
|
||||
{ text: "Fix Faces", on_click: onFixFacesClick },
|
||||
],
|
||||
{ text: "Use as Thumbnail", on_click: onUseAsThumbnailClick },
|
||||
]
|
||||
|
||||
// include the plugins
|
||||
@ -682,6 +690,127 @@ function onMakeSimilarClick(req, img) {
|
||||
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) {
|
||||
const imageSeed = img.getAttribute("data-seed")
|
||||
|
||||
@ -2594,31 +2723,52 @@ document.getElementById("toggle-tensorrt-install").addEventListener("click", fun
|
||||
/* Embeddings */
|
||||
|
||||
function updateEmbeddingsList(filter = "") {
|
||||
function html(model, prefix = "", filter = "") {
|
||||
function html(model, iconlist = [], prefix = "", filter = "") {
|
||||
filter = filter.toLowerCase()
|
||||
let toplevel = ""
|
||||
let folders = ""
|
||||
let toplevel = document.createElement("div")
|
||||
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) => {
|
||||
if (typeof m == "string") {
|
||||
if (m.toLowerCase().search(filter) != -1) {
|
||||
toplevel += `<button data-embedding="${m}">${m}</button> `
|
||||
let token=m.toLowerCase()
|
||||
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 {
|
||||
let subdir = html(m[1], prefix + m[0] + "/", filter)
|
||||
if (subdir != "") {
|
||||
folders +=
|
||||
`<div class="embedding-category"><h4 class="collapsible">${prefix}${m[0]}</h4><div class="collapsible-content">` +
|
||||
subdir +
|
||||
"</div></div>"
|
||||
let subdir = html(m[1], iconlist, prefix + m[0] + "/", filter)
|
||||
if (typeof(subdir) == "object") {
|
||||
let div1 = document.createElement("div")
|
||||
let div2 = document.createElement("div")
|
||||
div1.classList.add("collapsible-content")
|
||||
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) {
|
||||
let text = e.target.dataset["embedding"]
|
||||
let text = e.target.closest("[data-embedding]").dataset["embedding"]
|
||||
const insertIntoNegative = e.shiftKey || positiveEmbeddingText.classList.contains("displayNone")
|
||||
|
||||
if (embeddingsModeField.value == "insert") {
|
||||
@ -2643,14 +2793,38 @@ function updateEmbeddingsList(filter = "") {
|
||||
}
|
||||
}
|
||||
|
||||
embeddingsList.innerHTML = html(modelsOptions.embeddings, "", filter)
|
||||
embeddingsList.querySelectorAll("button").forEach((b) => {
|
||||
b.addEventListener("click", onButtonClick)
|
||||
})
|
||||
createCollapsibles(embeddingsList)
|
||||
if (filter != "") {
|
||||
embeddingsExpandAll()
|
||||
// Usually the rendering of the Embeddings HTML takes less than a second. In case it takes longer, show a spinner
|
||||
embeddingsList.innerHTML = `
|
||||
<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)
|
||||
if (filter != "") {
|
||||
embeddingsExpandAll()
|
||||
}
|
||||
resizeModifierCards(embeddingsCardSizeSelector.value)
|
||||
})
|
||||
}
|
||||
|
||||
function showEmbeddingDialog() {
|
||||
@ -2658,23 +2832,33 @@ function showEmbeddingDialog() {
|
||||
embeddingsSearchBox.value = ""
|
||||
embeddingsDialog.showModal()
|
||||
}
|
||||
|
||||
embeddingsButton.addEventListener("click", () => {
|
||||
positiveEmbeddingText.classList.remove("displayNone")
|
||||
negativeEmbeddingText.classList.add("displayNone")
|
||||
showEmbeddingDialog()
|
||||
})
|
||||
|
||||
negativeEmbeddingsButton.addEventListener("click", () => {
|
||||
positiveEmbeddingText.classList.add("displayNone")
|
||||
negativeEmbeddingText.classList.remove("displayNone")
|
||||
showEmbeddingDialog()
|
||||
})
|
||||
|
||||
embeddingsDialogCloseBtn.addEventListener("click", (e) => {
|
||||
embeddingsDialog.close()
|
||||
})
|
||||
|
||||
embeddingsSearchBox.addEventListener("input", (e) => {
|
||||
updateEmbeddingsList(embeddingsSearchBox.value)
|
||||
})
|
||||
|
||||
embeddingsCardSizeSelector.addEventListener("change", (e) => {
|
||||
resizeModifierCards(embeddingsCardSizeSelector.value)
|
||||
})
|
||||
|
||||
|
||||
|
||||
modalDialogCloseOnBackdropClick(embeddingsDialog)
|
||||
makeDialogDraggable(embeddingsDialog)
|
||||
|
||||
|
@ -194,6 +194,16 @@ var PARAMETERS = [
|
||||
icon: "fa-check-double",
|
||||
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",
|
||||
type: ParameterType.checkbox,
|
||||
@ -410,6 +420,7 @@ let useBetaChannelField = document.querySelector("#use_beta_channel")
|
||||
let uiOpenBrowserOnStartField = document.querySelector("#ui_open_browser_on_start")
|
||||
let confirmDangerousActionsField = document.querySelector("#confirm_dangerous_actions")
|
||||
let testDiffusers = document.querySelector("#test_diffusers")
|
||||
let profileNameField = document.querySelector("#profileName")
|
||||
|
||||
let saveSettingsBtn = document.querySelector("#save-system-settings-btn")
|
||||
|
||||
|
@ -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) {
|
||||
dialog.addEventListener('mousedown', function (event) {
|
||||
// Firefox creates an event with clientX|Y = 0|0 when choosing an <option>.
|
||||
|
Loading…
Reference in New Issue
Block a user