merge latest version of bucketlite branch

This commit is contained in:
JeLuF 2023-08-09 16:38:00 +02:00
commit 8dce1ed858
12 changed files with 1609 additions and 40 deletions

View File

@ -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.

View File

@ -71,9 +71,8 @@ def init():
bucket = crud.get_bucket_by_path(db, path)
if bucket == None:
bucket_id = crud.create_bucket(db=db, bucket=schemas.BucketCreate(path=path))
else:
bucket_id = bucket.id
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)

View File

@ -19,7 +19,6 @@ def create_bucketfile(db: Session, bucketfile: schemas.BucketFileCreate, bucket_
db_bucketfile = models.BucketFile(**bucketfile.dict(), bucket_id=bucket_id)
db.merge(db_bucketfile)
db.commit()
from pprint import pprint
db_bucketfile = db.query(models.BucketFile).filter(models.BucketFile.bucket_id==bucket_id, models.BucketFile.filename==bucketfile.filename).first()
return db_bucketfile

View File

@ -7,7 +7,6 @@ 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")
print("## SQLALCHEMY_DATABASE_URL = ", SQLALCHEMY_DATABASE_URL)
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

View File

@ -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">
@ -686,6 +688,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>&nbsp;<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">
@ -693,6 +704,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>

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

@ -1650,6 +1650,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;
}
@ -1741,6 +1770,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;
}
@ -1832,6 +1887,7 @@ div#enlarge-buttons {
#imageTagPopupContainer {
position: absolute;
}
/* Gallery CSS */
#imagecontainer {
display: flex;

View File

@ -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

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 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")
@ -678,20 +685,127 @@ function onMakeSimilarClick(req, img) {
createTask(newTaskRequest)
}
function onUseAsThumbnailClick(req, img) {
console.log(req)
console.log(img)
let embedding = prompt("Embedding name")
fetch(img.src)
.then(response => response.blob())
.then(async function(blob) {
const formData = new FormData()
formData.append("file", blob)
const response = await fetch(`bucket/embeddings/${embedding}.jpg`, { method: 'POST', body: formData });
console.log(response)
})
// 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")
@ -2524,40 +2638,53 @@ document.getElementById("toggle-tensorrt-install").addEventListener("click", fun
/* Embeddings */
let icl = []
function updateEmbeddingsList(filter = "") {
function html(model, iconlist = [], prefix = "", filter = "") {
filter = filter.toLowerCase()
let toplevel = ""
let folders = ""
console.log(iconlist)
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") {
let token=m.toLowerCase()
if (token.search(filter) != -1) {
let img = '/media/images/noimg.png'
if (token in embIcon) {
img = `/bucket/embeddings/${embIcon[token]}`
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)
}
toplevel += `<button data-embedding="${m}"><img src="${img}" height="128" width="128"><br>${m}</button> `
button.dataset["embedding"] = m
button.addEventListener("click", onButtonClick)
toplevel.appendChild(button)
}
} else {
let subdir = html(m[1], iconlist, 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>"
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.closest("button").dataset["embedding"]
let text = e.target.closest("[data-embedding]").dataset["embedding"]
const insertIntoNegative = e.shiftKey || positiveEmbeddingText.classList.contains("displayNone")
if (embeddingsModeField.value == "insert") {
@ -2582,8 +2709,18 @@ function updateEmbeddingsList(filter = "") {
}
}
// 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 = ""
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; ">
@ -2592,17 +2729,17 @@ function updateEmbeddingsList(filter = "") {
}
// END of remove block
fetch("/bucket/embeddings/")
.then(response => response.json())
.then(iconlist => {
embeddingsList.innerHTML = warning + html(modelsOptions.embeddings, iconlist, "", filter)
embeddingsList.querySelectorAll("button").forEach((b) => {
b.addEventListener("click", onButtonClick)
})
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)
})
}
@ -2611,23 +2748,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)

View File

@ -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")

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) {
dialog.addEventListener('mousedown', function (event) {
// Firefox creates an event with clientX|Y = 0|0 when choosing an <option>.