Merge pull request #1608 from easydiffusion/beta

LoRA Manager and Upload Thumbnails
This commit is contained in:
cmdr2 2023-09-27 19:24:41 +05:30 committed by GitHub
commit 6280a80129
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 923 additions and 505 deletions

View File

@ -17,6 +17,9 @@
- **Major rewrite of the code** - We've switched to using diffusers under-the-hood, which allows us to release new features faster, and focus on making the UI and installer even easier to use. - **Major rewrite of the code** - We've switched to using diffusers under-the-hood, which allows us to release new features faster, and focus on making the UI and installer even easier to use.
### Detailed changelog ### Detailed changelog
* 3.0.6 - 18 Sep 2023 - Add thumbnails to embeddings from the UI, using the new `Upload Thumbnail` button in the Embeddings popup. Thanks @JeLuf.
* 3.0.6 - 15 Sep 2023 - Fix broken embeddings dialog when LoRA information couldn't be fetched.
* 3.0.6 - 14 Sep 2023 - UI for adding notes to LoRA files (to help you remember which prompts to use). Also added a button to automatically fetch prompts from Civitai for a LoRA file, using the `Import from Civitai` button. Thanks @JeLuf.
* 3.0.5 - 2 Sep 2023 - Support SDXL ControlNets. * 3.0.5 - 2 Sep 2023 - Support SDXL ControlNets.
* 3.0.4 - 1 Sep 2023 - Fix incorrect metadata generated for embeddings, when the exact word doesn't match the case, or is part of a larger word. * 3.0.4 - 1 Sep 2023 - Fix incorrect metadata generated for embeddings, when the exact word doesn't match the case, or is part of a larger word.
* 3.0.4 - 1 Sep 2023 - Simplify the installation for AMD users on Linux. Thanks @JeLuf. * 3.0.4 - 1 Sep 2023 - Simplify the installation for AMD users on Linux. Thanks @JeLuf.

View File

@ -21,7 +21,7 @@ os_name = platform.system()
modules_to_check = { modules_to_check = {
"torch": ("1.11.0", "1.13.1", "2.0.0", "2.0.1"), "torch": ("1.11.0", "1.13.1", "2.0.0", "2.0.1"),
"torchvision": ("0.12.0", "0.14.1", "0.15.1", "0.15.2"), "torchvision": ("0.12.0", "0.14.1", "0.15.1", "0.15.2"),
"sdkit": "2.0.10", "sdkit": "2.0.12",
"stable-diffusion-sdkit": "2.1.4", "stable-diffusion-sdkit": "2.1.4",
"rich": "12.6.0", "rich": "12.6.0",
"uvicorn": "0.19.0", "uvicorn": "0.19.0",

View File

@ -34,6 +34,7 @@ call conda activate
@REM remove the old version of the dev console script, if it's still present @REM remove the old version of the dev console script, if it's still present
if exist "Open Developer Console.cmd" del "Open Developer Console.cmd" if exist "Open Developer Console.cmd" del "Open Developer Console.cmd"
if exist "ui\plugins\ui\merge.plugin.js" del "ui\plugins\ui\merge.plugin.js"
@rem create the stable-diffusion folder, to work with legacy installations @rem create the stable-diffusion folder, to work with legacy installations
if not exist "stable-diffusion" mkdir stable-diffusion if not exist "stable-diffusion" mkdir stable-diffusion

View File

@ -7,6 +7,7 @@ cp sd-ui-files/scripts/check_modules.py scripts/
cp sd-ui-files/scripts/get_config.py scripts/ cp sd-ui-files/scripts/get_config.py scripts/
cp sd-ui-files/scripts/config.yaml.sample scripts/ cp sd-ui-files/scripts/config.yaml.sample scripts/
source ./scripts/functions.sh source ./scripts/functions.sh
# activate the installer env # activate the installer env
@ -20,6 +21,10 @@ if [ -e "open_dev_console.sh" ]; then
rm "open_dev_console.sh" rm "open_dev_console.sh"
fi fi
if [ -e "ui/plugins/ui/merge.plugin.js" ]; then
rm "ui/plugins/ui/merge.plugin.js"
fi
# set the correct installer path (current vs legacy) # set the correct installer path (current vs legacy)
if [ -e "installer_files/env" ]; then if [ -e "installer_files/env" ]; then
export INSTALL_ENV_DIR="$(pwd)/installer_files/env" export INSTALL_ENV_DIR="$(pwd)/installer_files/env"

View File

@ -55,8 +55,13 @@ def init():
return bucketfiles return bucketfiles
else: else:
bucket_id = crud.get_bucket_by_path(db, path).id bucket = crud.get_bucket_by_path(db, path)
if bucket == None:
raise HTTPException(status_code=404, detail="Bucket not found")
bucket_id = bucket.id
bucketfile = db.query(models.BucketFile).filter(models.BucketFile.bucket_id == bucket_id, models.BucketFile.filename == filename).first() bucketfile = db.query(models.BucketFile).filter(models.BucketFile.bucket_id == bucket_id, models.BucketFile.filename == filename).first()
if bucketfile == None:
raise HTTPException(status_code=404, detail="File not found")
suffix = get_suffix_from_filename(filename) suffix = get_suffix_from_filename(filename)

View File

@ -470,7 +470,6 @@ def modify_package_internal(package_name: str, req: dict):
def get_sha256_internal(obj_path): def get_sha256_internal(obj_path):
import hashlib
from easydiffusion.utils import sha256sum from easydiffusion.utils import sha256sum
path = obj_path.split("/") path = obj_path.split("/")

View File

@ -1,4 +1,5 @@
import logging import logging
import hashlib
log = logging.getLogger("easydiffusion") log = logging.getLogger("easydiffusion")

View File

@ -35,7 +35,7 @@
<h1> <h1>
<img id="logo_img" src="/media/images/icon-512x512.png" > <img id="logo_img" src="/media/images/icon-512x512.png" >
Easy Diffusion Easy Diffusion
<small><span id="version">v3.0.5</span> <span id="updateBranchLabel"></span></small> <small><span id="version">v3.0.6</span> <span id="updateBranchLabel"></span></small>
</h1> </h1>
</div> </div>
<div id="server-status"> <div id="server-status">
@ -291,7 +291,9 @@
<option value="2048">2048</option> <option value="2048">2048</option>
</select> </select>
<label id="widthLabel" for="width"><small><span>(width)</span></small></label> <label id="widthLabel" for="width"><small><span>(width)</span></small></label>
<span id="swap-width-height" class="clickable smallButton" style="margin-left: 2px; margin-right:2px;"><i class="fa-solid fa-right-left"><span class="simple-tooltip top-left"> Swap width and height </span></i></span> <div class="tooltip-container">
<span id="swap-width-height" class="clickable smallButton" style="margin-left: 2px; margin-right:2px;"><i class="fa-solid fa-right-left"><span class="simple-tooltip top-left"> Swap width and height </span></i></span>
</div>
<select id="height" name="height" value="512"> <select id="height" name="height" value="512">
<option value="128">128</option> <option value="128">128</option>
<option value="192">192</option> <option value="192">192</option>

View File

@ -1214,6 +1214,12 @@ input::file-selector-button {
visibility: visible; visibility: visible;
} }
} }
.tooltip-container {
display: inline-block;
position: relative;
}
.simple-tooltip.right { .simple-tooltip.right {
right: 0px; right: 0px;
top: 50%; top: 50%;

View File

@ -514,7 +514,7 @@ function showImages(reqBody, res, outputContainer, livePreview) {
{ {
text: "Use as Thumbnail", text: "Use as Thumbnail",
on_click: onUseAsThumbnailClick, on_click: onUseAsThumbnailClick,
filter: (req, img) => "use_embeddings_model" in req, filter: (req, img) => "use_embeddings_model" in req || "use_lora_model" in req
}, },
] ]
@ -748,25 +748,45 @@ function onUseAsThumbnailClick(req, img) {
onUseAsThumbnailClick.croppr.setImage(img.src) onUseAsThumbnailClick.croppr.setImage(img.src)
} }
let embeddings = req.use_embeddings_model.map((e) => e.split("/").pop()) useAsThumbSelect.innerHTML=""
let LORA = []
if ("use_lora_model" in req) { if ("use_embeddings_model" in req) {
LORA = req.use_lora_model let embeddings = req.use_embeddings_model.map((e) => e.split("/").pop())
let embOptions = document.createElement("optgroup")
embOptions.label = "Embeddings"
embOptions.replaceChildren(
...embeddings.map((e) => {
let option = document.createElement("option")
option.innerText = e
option.dataset["type"] = "embeddings"
return option
})
)
useAsThumbSelect.appendChild(embOptions)
} }
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) if ("use_lora_model" in req) {
let LORA = req.use_lora_model
if (typeof LORA == "string") {
LORA = [LORA]
}
LORA = LORA.map((e) => e.split("/").pop())
let loraOptions = document.createElement("optgroup")
loraOptions.label = "LORA"
loraOptions.replaceChildren(
...LORA.map((e) => {
let option = document.createElement("option")
option.innerText = e
option.dataset["type"] = "lora"
return option
})
)
useAsThumbSelect.appendChild(loraOptions)
}
useAsThumbDialog.showModal() useAsThumbDialog.showModal()
onUseAsThumbnailClick.scale = scale onUseAsThumbnailClick.scale = scale
} }
@ -782,6 +802,50 @@ useAsThumbCancelBtn.addEventListener("click", () => {
useAsThumbDialog.close() useAsThumbDialog.close()
}) })
const Bucket = {
upload(path, blob) {
const formData = new FormData()
formData.append("file", blob)
return fetch(`bucket/${path}`, {
method: "POST",
body: formData,
})
},
getImageAsDataURL(path) {
return fetch(`bucket/${path}`)
.then((response) => {
if (response.status == 200) {
return response.blob()
} else {
throw new Error("Bucket error")
}
})
.then((blob) => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result)
reader.onerror = reject
reader.readAsDataURL(blob)
})
})
},
getList(path) {
return fetch(`bucket/${path}`)
.then((response) => (response.status == 200 ? response.json() : []))
},
store(path, data) {
return Bucket.upload(`${path}.json`, JSON.stringify(data))
},
retrieve(path) {
return fetch(`bucket/${path}.json`)
.then((response) => (response.status == 200 ? response.json() : null))
},
}
useAsThumbSaveBtn.addEventListener("click", (e) => { useAsThumbSaveBtn.addEventListener("click", (e) => {
let scale = 1 / onUseAsThumbnailClick.scale let scale = 1 / onUseAsThumbnailClick.scale
let crop = onUseAsThumbnailClick.croppr.getValue() let crop = onUseAsThumbnailClick.croppr.getValue()
@ -793,22 +857,18 @@ useAsThumbSaveBtn.addEventListener("click", (e) => {
.then((thumb) => fetch(thumb)) .then((thumb) => fetch(thumb))
.then((response) => response.blob()) .then((response) => response.blob())
.then(async function(blob) { .then(async function(blob) {
const formData = new FormData()
formData.append("file", blob)
let options = useAsThumbSelect.selectedOptions let options = useAsThumbSelect.selectedOptions
let promises = [] let promises = []
for (let embedding of options) { for (let embedding of options) {
promises.push( promises.push(
fetch(`bucket/${profileName}/${embedding.dataset["type"]}/${embedding.value}.png`, { Bucket.upload(`${profileName}/${embedding.dataset["type"]}/${embedding.value}.png`, blob)
method: "POST",
body: formData,
})
) )
} }
return Promise.all(promises) return Promise.all(promises)
}) })
.then(() => { .then(() => {
useAsThumbDialog.close() useAsThumbDialog.close()
document.dispatchEvent(new CustomEvent("saveThumb", { detail: useAsThumbSelect.selectedOptions }))
}) })
.catch((error) => { .catch((error) => {
console.error(error) console.error(error)
@ -2362,20 +2422,10 @@ function loadThumbnailImageFromFile() {
} }
function updateEmbeddingsList(filter = "") { function updateEmbeddingsList(filter = "") {
function html(model, iconlist = [], prefix = "", filter = "") { function html(model, iconMap = {}, prefix = "", filter = "") {
filter = filter.toLowerCase() filter = filter.toLowerCase()
let toplevel = document.createElement("div") let toplevel = document.createElement("div")
let folders = 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 let profileName = profileNameField.value
model?.forEach((m) => { model?.forEach((m) => {
@ -2383,13 +2433,9 @@ function updateEmbeddingsList(filter = "") {
let token = m.toLowerCase() let token = m.toLowerCase()
if (token.search(filter) != -1) { if (token.search(filter) != -1) {
let button let button
// if (iconlist.length==0) {
// button = document.createElement("button")
// button.innerText = m
// } else {
let img = "/media/images/noimg.png" let img = "/media/images/noimg.png"
if (token in embIcon) { if (token in iconMap) {
img = `/bucket/${profileName}/embeddings/${embIcon[token]}` img = `/bucket/${profileName}/${iconMap[token]}`
} }
button = createModifierCard(m, [img, img], true) button = createModifierCard(m, [img, img], true)
// } // }
@ -2398,7 +2444,7 @@ function updateEmbeddingsList(filter = "") {
toplevel.appendChild(button) toplevel.appendChild(button)
} }
} else { } else {
let subdir = html(m[1], iconlist, prefix + m[0] + "/", filter) let subdir = html(m[1], iconMap, prefix + m[0] + "/", filter)
if (typeof subdir == "object") { if (typeof subdir == "object") {
let div1 = document.createElement("div") let div1 = document.createElement("div")
let div2 = document.createElement("div") let div2 = document.createElement("div")
@ -2457,11 +2503,44 @@ function updateEmbeddingsList(filter = "") {
</div> </div>
` `
let loraTokens = []
let profileName = profileNameField.value let profileName = profileNameField.value
fetch(`/bucket/${profileName}/embeddings/`) let iconMap = {}
.then((response) => (response.status == 200 ? response.json() : []))
.then(async function(iconlist) { Bucket.getList(`${profileName}/embeddings/`)
embeddingsList.replaceChildren(html(modelsOptions.embeddings, iconlist, "", filter)) .then((icons) => {
iconMap = Object.assign(
{},
...icons.map((x) => ({
[x
.toLowerCase()
.split(".")
.slice(0, -1)
.join(".")]: `embeddings/${x}`,
}))
)
return Bucket.getList(`${profileName}/lora/`)
})
.then(async function (icons) {
for (let lora of loraModelField.value.modelNames) {
let keywords = await getLoraKeywords(lora)
loraTokens = loraTokens.concat(keywords)
let loraname = lora.split("/").pop()
if (icons.includes(`${loraname}.png`)) {
keywords.forEach((kw) => {
iconMap[kw.toLowerCase()] = `lora/${loraname}.png`
})
}
}
let tokenList = [...modelsOptions.embeddings]
if (loraTokens.length != 0) {
tokenList.unshift(['LORA Keywords', loraTokens])
}
embeddingsList.replaceChildren(html(tokenList, iconMap, "", filter))
createCollapsibles(embeddingsList) createCollapsibles(embeddingsList)
if (filter != "") { if (filter != "") {
embeddingsExpandAll() embeddingsExpandAll()

View File

@ -737,6 +737,7 @@ async function getSystemInfo() {
if (force == true) { if (force == true) {
saveToDiskField.checked = true saveToDiskField.checked = true
metadataOutputFormatField.disabled = false metadataOutputFormatField.disabled = false
metadataOutputFormatField.value = "json"
} }
saveToDiskField.disabled = force saveToDiskField.disabled = force
diskPathField.disabled = force diskPathField.disabled = force

View File

@ -1,454 +0,0 @@
;(function() {
"use strict"
///////////////////// Function section
function smoothstep(x) {
return x * x * (3 - 2 * x)
}
function smootherstep(x) {
return x * x * x * (x * (x * 6 - 15) + 10)
}
function smootheststep(x) {
let y = -20 * Math.pow(x, 7)
y += 70 * Math.pow(x, 6)
y -= 84 * Math.pow(x, 5)
y += 35 * Math.pow(x, 4)
return y
}
function getCurrentTime() {
const now = new Date()
let hours = now.getHours()
let minutes = now.getMinutes()
let seconds = now.getSeconds()
hours = hours < 10 ? `0${hours}` : hours
minutes = minutes < 10 ? `0${minutes}` : minutes
seconds = seconds < 10 ? `0${seconds}` : seconds
return `${hours}:${minutes}:${seconds}`
}
function addLogMessage(message) {
const logContainer = document.getElementById("merge-log")
logContainer.innerHTML += `<i>${getCurrentTime()}</i> ${message}<br>`
// Scroll to the bottom of the log
logContainer.scrollTop = logContainer.scrollHeight
document.querySelector("#merge-log-container").style.display = "block"
}
function addLogSeparator() {
const logContainer = document.getElementById("merge-log")
logContainer.innerHTML += "<hr>"
logContainer.scrollTop = logContainer.scrollHeight
}
function drawDiagram(fn) {
const SIZE = 300
const canvas = document.getElementById("merge-canvas")
canvas.height = canvas.width = SIZE
const ctx = canvas.getContext("2d")
// Draw coordinate system
ctx.scale(1, -1)
ctx.translate(0, -canvas.height)
ctx.lineWidth = 1
ctx.beginPath()
ctx.strokeStyle = "white"
ctx.moveTo(0, 0)
ctx.lineTo(0, SIZE)
ctx.lineTo(SIZE, SIZE)
ctx.lineTo(SIZE, 0)
ctx.lineTo(0, 0)
ctx.lineTo(SIZE, SIZE)
ctx.stroke()
ctx.beginPath()
ctx.setLineDash([1, 2])
const n = SIZE / 10
for (let i = n; i < SIZE; i += n) {
ctx.moveTo(0, i)
ctx.lineTo(SIZE, i)
ctx.moveTo(i, 0)
ctx.lineTo(i, SIZE)
}
ctx.stroke()
ctx.beginPath()
ctx.setLineDash([])
ctx.beginPath()
ctx.strokeStyle = "black"
ctx.lineWidth = 3
// Plot function
const numSamples = 20
for (let i = 0; i <= numSamples; i++) {
const x = i / numSamples
const y = fn(x)
const canvasX = x * SIZE
const canvasY = y * SIZE
if (i === 0) {
ctx.moveTo(canvasX, canvasY)
} else {
ctx.lineTo(canvasX, canvasY)
}
}
ctx.stroke()
// Plot alpha values (yellow boxes)
let start = parseFloat(document.querySelector("#merge-start").value)
let step = parseFloat(document.querySelector("#merge-step").value)
let iterations = document.querySelector("#merge-count").value >> 0
ctx.beginPath()
ctx.fillStyle = "yellow"
for (let i = 0; i < iterations; i++) {
const alpha = (start + i * step) / 100
const x = alpha * SIZE
const y = fn(alpha) * SIZE
if (x <= SIZE) {
ctx.rect(x - 3, y - 3, 6, 6)
ctx.fill()
} else {
ctx.strokeStyle = "red"
ctx.moveTo(0, 0)
ctx.lineTo(0, SIZE)
ctx.lineTo(SIZE, SIZE)
ctx.lineTo(SIZE, 0)
ctx.lineTo(0, 0)
ctx.lineTo(SIZE, SIZE)
ctx.stroke()
addLogMessage("<i>Warning: maximum ratio is &#8805; 100%</i>")
}
}
}
function updateChart() {
let fn = (x) => x
switch (document.querySelector("#merge-interpolation").value) {
case "SmoothStep":
fn = smoothstep
break
case "SmootherStep":
fn = smootherstep
break
case "SmoothestStep":
fn = smootheststep
break
}
drawDiagram(fn)
}
createTab({
id: "merge",
icon: "fa-code-merge",
label: "Merge models",
css: `
#tab-content-merge .tab-content-inner {
max-width: 100%;
padding: 10pt;
}
.merge-container {
margin-left: 15%;
margin-right: 15%;
text-align: left;
display: inline-grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto auto;
gap: 0px 0px;
grid-auto-flow: row;
grid-template-areas:
"merge-input merge-config"
"merge-buttons merge-buttons";
}
.merge-container p {
margin-top: 3pt;
margin-bottom: 3pt;
}
.merge-config .tab-content {
background: var(--background-color1);
border-radius: 3pt;
}
.merge-config .tab-content-inner {
text-align: left;
}
.merge-input {
grid-area: merge-input;
padding-left:1em;
}
.merge-config {
grid-area: merge-config;
padding:1em;
}
.merge-config input {
margin-bottom: 3px;
}
.merge-config select {
margin-bottom: 3px;
}
.merge-buttons {
grid-area: merge-buttons;
padding:1em;
text-align: center;
}
#merge-button {
padding: 8px;
width:20em;
}
div#merge-log {
height:150px;
overflow-x:hidden;
overflow-y:scroll;
background:var(--background-color1);
border-radius: 3pt;
}
div#merge-log i {
color: hsl(var(--accent-hue), 100%, calc(2*var(--accent-lightness)));
font-family: monospace;
}
.disabled {
background: var(--background-color4);
color: var(--text-color);
}
#merge-type-tabs {
border-bottom: 1px solid black;
}
#merge-log-container {
display: none;
}
.merge-container #merge-warning {
color: rgb(153, 153, 153);
}`,
content: `
<div class="merge-container panel-box">
<div class="merge-input">
<p><label for="#mergeModelA">Select Model A:</label></p>
<input id="mergeModelA" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
<p><label for="#mergeModelB">Select Model B:</label></p>
<input id="mergeModelB" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
<br/><br/>
<p id="merge-warning"><small><b>Important:</b> Please merge models of similar type.<br/>For e.g. <code>SD 1.4</code> models with only <code>SD 1.4/1.5</code> models,<br/><code>SD 2.0</code> with <code>SD 2.0</code>-type, and <code>SD 2.1</code> with <code>SD 2.1</code>-type models.</small></p>
<br/>
<table>
<tr>
<td><label for="#merge-filename">Output file name:</label></td>
<td><input id="merge-filename" size=24> <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Base name of the output file.<br>Mix ratio and file suffix will be appended to this.</span></i></td>
</tr>
<tr>
<td><label for="#merge-fp">Output precision:</label></td>
<td><select id="merge-fp">
<option value="fp16">fp16 (smaller file size)</option>
<option value="fp32">fp32 (larger file size)</option>
</select>
<i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Image generation uses fp16, so it's a good choice.<br>Use fp32 if you want to use the result models for more mixes</span></i>
</td>
</tr>
<tr>
<td><label for="#merge-format">Output file format:</label></td>
<td><select id="merge-format">
<option value="safetensors">Safetensors (recommended)</option>
<option value="ckpt">CKPT/Pickle (legacy format)</option>
</select>
</td>
</tr>
</table>
<br/>
<div id="merge-log-container">
<p><label for="#merge-log">Log messages:</label></p>
<div id="merge-log"></div>
</div>
</div>
<div class="merge-config">
<div class="tab-container">
<span id="tab-merge-opts-single" class="tab active">
<span>Make a single file</small></span>
</span>
<span id="tab-merge-opts-batch" class="tab">
<span>Make multiple variations</small></span>
</span>
</div>
<div>
<div id="tab-content-merge-opts-single" class="tab-content active">
<div class="tab-content-inner">
<small>Saves a single merged model file, at the specified merge ratio.</small><br/><br/>
<label for="#single-merge-ratio-slider">Merge ratio:</label>
<input id="single-merge-ratio-slider" name="single-merge-ratio-slider" class="editor-slider" value="50" type="range" min="1" max="1000">
<input id="single-merge-ratio" size=2 value="5">%
<i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Model A's contribution to the mix. The rest will be from Model B.</span></i>
</div>
</div>
<div id="tab-content-merge-opts-batch" class="tab-content">
<div class="tab-content-inner">
<small>Saves multiple variations of the model, at different merge ratios.<br/>Each variation will be saved as a separate file.</small><br/><br/>
<table>
<tr><td><label for="#merge-count">Number of variations:</label></td>
<td> <input id="merge-count" size=2 value="5"></td>
<td> <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Number of models to create</span></i></td></tr>
<tr><td><label for="#merge-start">Starting merge ratio:</label></td>
<td> <input id="merge-start" size=2 value="5">%</td>
<td> <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Smallest share of model A in the mix</span></i></td></tr>
<tr><td><label for="#merge-step">Increment each step:</label></td>
<td> <input id="merge-step" size=2 value="10">%</td>
<td> <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Share of model A added into the mix per step</span></i></td></tr>
<tr><td><label for="#merge-interpolation">Interpolation model:</label></td>
<td> <select id="merge-interpolation">
<option>Exact</option>
<option>SmoothStep</option>
<option>SmootherStep</option>
<option>SmoothestStep</option>
</select></td>
<td> <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Sigmoid function to be applied to the model share before mixing</span></i></td></tr>
</table>
<br/>
<small>Preview of variation ratios:</small><br/>
<canvas id="merge-canvas" width="400" height="400"></canvas>
</div>
</div>
</div>
</div>
<div class="merge-buttons">
<button id="merge-button" class="primaryButton">Merge models</button>
</div>
</div>`,
onOpen: ({ firstOpen }) => {
if (!firstOpen) {
return
}
const tabSettingsSingle = document.querySelector("#tab-merge-opts-single")
const tabSettingsBatch = document.querySelector("#tab-merge-opts-batch")
linkTabContents(tabSettingsSingle)
linkTabContents(tabSettingsBatch)
console.log("Activate")
let mergeModelAField = new ModelDropdown(document.querySelector("#mergeModelA"), "stable-diffusion")
let mergeModelBField = new ModelDropdown(document.querySelector("#mergeModelB"), "stable-diffusion")
updateChart()
// slider
const singleMergeRatioField = document.querySelector("#single-merge-ratio")
const singleMergeRatioSlider = document.querySelector("#single-merge-ratio-slider")
function updateSingleMergeRatio() {
singleMergeRatioField.value = singleMergeRatioSlider.value / 10
singleMergeRatioField.dispatchEvent(new Event("change"))
}
function updateSingleMergeRatioSlider() {
if (singleMergeRatioField.value < 0) {
singleMergeRatioField.value = 0
} else if (singleMergeRatioField.value > 100) {
singleMergeRatioField.value = 100
}
singleMergeRatioSlider.value = singleMergeRatioField.value * 10
singleMergeRatioSlider.dispatchEvent(new Event("change"))
}
singleMergeRatioSlider.addEventListener("input", updateSingleMergeRatio)
singleMergeRatioField.addEventListener("input", updateSingleMergeRatioSlider)
updateSingleMergeRatio()
document.querySelector(".merge-config").addEventListener("change", updateChart)
document.querySelector("#merge-button").addEventListener("click", async function(e) {
// Build request template
let model0 = mergeModelAField.value
let model1 = mergeModelBField.value
let request = { model0: model0, model1: model1 }
request["use_fp16"] = document.querySelector("#merge-fp").value == "fp16"
let iterations = document.querySelector("#merge-count").value >> 0
let start = parseFloat(document.querySelector("#merge-start").value)
let step = parseFloat(document.querySelector("#merge-step").value)
if (isTabActive(tabSettingsSingle)) {
start = parseFloat(singleMergeRatioField.value)
step = 0
iterations = 1
addLogMessage(`merge ratio = ${start}%`)
} else {
addLogMessage(`start = ${start}%`)
addLogMessage(`step = ${step}%`)
}
if (start + (iterations - 1) * step >= 100) {
addLogMessage("<i>Aborting: maximum ratio is &#8805; 100%</i>")
addLogMessage("Reduce the number of variations or the step size")
addLogSeparator()
document.querySelector("#merge-count").focus()
return
}
if (document.querySelector("#merge-filename").value == "") {
addLogMessage("<i>Aborting: No output file name specified</i>")
addLogSeparator()
document.querySelector("#merge-filename").focus()
return
}
// Disable merge button
e.target.disabled = true
e.target.classList.add("disabled")
let cursor = $("body").css("cursor")
let label = document.querySelector("#merge-button").innerHTML
$("body").css("cursor", "progress")
document.querySelector("#merge-button").innerHTML = "Merging models ..."
addLogMessage("Merging models")
addLogMessage("Model A: " + model0)
addLogMessage("Model B: " + model1)
// Batch main loop
for (let i = 0; i < iterations; i++) {
let alpha = (start + i * step) / 100
if (isTabActive(tabSettingsBatch)) {
switch (document.querySelector("#merge-interpolation").value) {
case "SmoothStep":
alpha = smoothstep(alpha)
break
case "SmootherStep":
alpha = smootherstep(alpha)
break
case "SmoothestStep":
alpha = smootheststep(alpha)
break
}
}
addLogMessage(`merging batch job ${i + 1}/${iterations}, alpha = ${alpha.toFixed(5)}...`)
request["out_path"] = document.querySelector("#merge-filename").value
request["out_path"] += "-" + alpha.toFixed(5) + "." + document.querySelector("#merge-format").value
addLogMessage(`&nbsp;&nbsp;filename: ${request["out_path"]}`)
// sdkit documentation: "ratio - the ratio of the second model. 1 means only the second model will be used."
request["ratio"] = 1-alpha
let res = await fetch("/model/merge", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
})
const data = await res.json()
addLogMessage(JSON.stringify(data))
}
addLogMessage(
"<b>Done.</b> The models have been saved to your <tt>models/stable-diffusion</tt> folder."
)
addLogSeparator()
// Re-enable merge button
$("body").css("cursor", cursor)
document.querySelector("#merge-button").innerHTML = label
e.target.disabled = false
e.target.classList.remove("disabled")
// Update model list
stableDiffusionModelField.innerHTML = ""
vaeModelField.innerHTML = ""
hypernetworkModelField.innerHTML = ""
await getModels()
})
},
})
})()

View File

@ -0,0 +1,770 @@
;(function() {
"use strict"
let mergeCSS = `
/*********** Main tab ***********/
.tab-centered {
justify-content: center;
}
#model-tool-tab-content {
background-color: var(--background-color3);
}
#model-tool-tab-content .tab-content-inner {
text-align: initial;
}
#model-tool-tab-bar .tab {
margin-bottom: 0px;
border-top-left-radius: var(--input-border-radius);
background-color: var(--background-color3);
padding: 6px 6px 0.8em 6px;
}
#tab-content-merge .tab-content-inner {
max-width: 100%;
padding: 10pt;
}
/*********** Merge UI ***********/
.merge-model-container {
margin-left: 15%;
margin-right: 15%;
text-align: left;
display: inline-grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto auto;
gap: 0px 0px;
grid-auto-flow: row;
grid-template-areas:
"merge-input merge-config"
"merge-buttons merge-buttons";
}
.merge-model-container p {
margin-top: 3pt;
margin-bottom: 3pt;
}
.merge-config .tab-content {
background: var(--background-color1);
border-radius: 3pt;
}
.merge-config .tab-content-inner {
text-align: left;
}
.merge-input {
grid-area: merge-input;
padding-left:1em;
}
.merge-config {
grid-area: merge-config;
padding:1em;
}
.merge-config input {
margin-bottom: 3px;
}
.merge-config select {
margin-bottom: 3px;
}
.merge-buttons {
grid-area: merge-buttons;
padding:1em;
text-align: center;
}
#merge-button {
padding: 8px;
width:20em;
}
div#merge-log {
height:150px;
overflow-x:hidden;
overflow-y:scroll;
background:var(--background-color1);
border-radius: 3pt;
}
div#merge-log i {
color: hsl(var(--accent-hue), 100%, calc(2*var(--accent-lightness)));
font-family: monospace;
}
.disabled {
background: var(--background-color4);
color: var(--text-color);
}
#merge-type-tabs {
border-bottom: 1px solid black;
}
#merge-log-container {
display: none;
}
.merge-model-container #merge-warning {
color: var(--small-label-color);
}
/*********** LORA UI ***********/
.lora-manager-grid {
display: grid;
gap: 0px 8px;
grid-auto-flow: row;
}
@media screen and (min-width: 1501px) {
.lora-manager-grid textarea {
height:350px;
}
.lora-manager-grid {
grid-template-columns: auto 1fr 1fr;
grid-template-rows: auto 1fr;
grid-template-areas:
"selector selector selector"
"thumbnail keywords notes";
}
}
@media screen and (min-width: 1001px) and (max-width: 1500px) {
.lora-manager-grid textarea {
height:250px;
}
.lora-manager-grid {
grid-template-columns: auto auto;
grid-template-rows: auto auto auto;
grid-template-areas:
"selector selector"
"thumbnail keywords"
"thumbnail notes";
}
}
@media screen and (max-width: 1000px) {
.lora-manager-grid textarea {
height:200px;
}
.lora-manager-grid {
grid-template-columns: auto;
grid-template-rows: auto auto auto auto;
grid-template-areas:
"selector"
"keywords"
"thumbnail"
"notes";
}
}
.lora-manager-grid-selector {
grid-area: selector;
justify-self: start;
}
.lora-manager-grid-thumbnail {
grid-area: thumbnail;
justify-self: center;
}
.lora-manager-grid-keywords {
grid-area: keywords;
}
.lora-manager-grid-notes {
grid-area: notes;
}
.lora-manager-grid p {
margin-bottom: 2px;
}
`
let mergeUI = `
<div class="merge-model-container panel-box">
<div class="merge-input">
<p><label for="#mergeModelA">Select Model A:</label></p>
<input id="mergeModelA" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
<p><label for="#mergeModelB">Select Model B:</label></p>
<input id="mergeModelB" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
<br/><br/>
<p id="merge-warning"><small><b>Important:</b> Please merge models of similar type.<br/>For e.g. <code>SD 1.4</code> models with only <code>SD 1.4/1.5</code> models,<br/><code>SD 2.0</code> with <code>SD 2.0</code>-type, and <code>SD 2.1</code> with <code>SD 2.1</code>-type models.</small></p>
<br/>
<table>
<tr>
<td><label for="#merge-filename">Output file name:</label></td>
<td><input id="merge-filename" size=24> <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Base name of the output file.<br>Mix ratio and file suffix will be appended to this.</span></i></td>
</tr>
<tr>
<td><label for="#merge-fp">Output precision:</label></td>
<td><select id="merge-fp">
<option value="fp16">fp16 (smaller file size)</option>
<option value="fp32">fp32 (larger file size)</option>
</select>
<i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Image generation uses fp16, so it's a good choice.<br>Use fp32 if you want to use the result models for more mixes</span></i>
</td>
</tr>
<tr>
<td><label for="#merge-format">Output file format:</label></td>
<td><select id="merge-format">
<option value="safetensors">Safetensors (recommended)</option>
<option value="ckpt">CKPT/Pickle (legacy format)</option>
</select>
</td>
</tr>
</table>
<br/>
<div id="merge-log-container">
<p><label for="#merge-log">Log messages:</label></p>
<div id="merge-log"></div>
</div>
</div>
<div class="merge-config">
<div class="tab-container">
<span id="tab-merge-opts-single" class="tab active">
<span>Make a single file</small></span>
</span>
<span id="tab-merge-opts-batch" class="tab">
<span>Make multiple variations</small></span>
</span>
</div>
<div>
<div id="tab-content-merge-opts-single" class="tab-content active">
<div class="tab-content-inner">
<small>Saves a single merged model file, at the specified merge ratio.</small><br/><br/>
<label for="#single-merge-ratio-slider">Merge ratio:</label>
<input id="single-merge-ratio-slider" name="single-merge-ratio-slider" class="editor-slider" value="50" type="range" min="1" max="1000">
<input id="single-merge-ratio" size=2 value="5">%
<i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Model A's contribution to the mix. The rest will be from Model B.</span></i>
</div>
</div>
<div id="tab-content-merge-opts-batch" class="tab-content">
<div class="tab-content-inner">
<small>Saves multiple variations of the model, at different merge ratios.<br/>Each variation will be saved as a separate file.</small><br/><br/>
<table>
<tr><td><label for="#merge-count">Number of variations:</label></td>
<td> <input id="merge-count" size=2 value="5"></td>
<td> <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Number of models to create</span></i></td></tr>
<tr><td><label for="#merge-start">Starting merge ratio:</label></td>
<td> <input id="merge-start" size=2 value="5">%</td>
<td> <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Smallest share of model A in the mix</span></i></td></tr>
<tr><td><label for="#merge-step">Increment each step:</label></td>
<td> <input id="merge-step" size=2 value="10">%</td>
<td> <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Share of model A added into the mix per step</span></i></td></tr>
<tr><td><label for="#merge-interpolation">Interpolation model:</label></td>
<td> <select id="merge-interpolation">
<option>Exact</option>
<option>SmoothStep</option>
<option>SmootherStep</option>
<option>SmoothestStep</option>
</select></td>
<td> <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Sigmoid function to be applied to the model share before mixing</span></i></td></tr>
</table>
<br/>
<small>Preview of variation ratios:</small><br/>
<canvas id="merge-canvas" width="400" height="400"></canvas>
</div>
</div>
</div>
</div>
<div class="merge-buttons">
<button id="merge-button" class="primaryButton">Merge models</button>
</div>
</div>`
let loraUI=`
<div class="panel-box lora-manager-grid">
<div class="lora-manager-grid-selector">
<label for="#loraModel">Select Lora:</label>
<input id="loraModel" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
</div>
<div class="lora-manager-grid-thumbnail">
<p style="height:2em;">Thumbnail:</p>
<div style="position:relative; height:256px; width:256px;background-color:#222;border-radius:1em;margin-bottom:1em;">
<i id="lora-manager-image-placeholder" class="fa-regular fa-image" style="font-size:500%;color:#555;position:absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);"></i>
<img id="lora-manager-image" class="displayNone" style="border-radius:6px;max-height:256px;max-width:256px;"/>
</div>
<div style="text-align:center;">
<button class="tertiaryButton" id="lora-manager-upload-button"><i class="fa-solid fa-upload"></i> Upload new thumbnail</button>
<input id="lora-manager-upload-input" name="lora-manager-upload-input" type="file" class="displayNone">
<!-- button class="tertiaryButton"><i class="fa-solid fa-trash-can"></i> Remove</button -->
</div>
</div>
<div class="lora-manager-grid-keywords">
<p style="height:2em;">Keywords:
<span style="float:right;margin-bottom:4px;"><button id="lora-keyword-from-civitai" class="tertiaryButton smallButton">Import from Civitai</button></span></p>
<textarea style="width:100%;resize:vertical;" id="lora-manager-keywords" placeholder="Put LORA specific keywords here..."></textarea>
<p style="color:var(--small-label-color);">
<b>LORA model keywords</b> can be used via the <code>+&nbsp;Embeddings</code> button. They get added to the embedding
keyword menu when the LORA has been selected in the image settings.
</p>
</div>
<div class="lora-manager-grid-notes">
<p style="height:2em;">Notes:</p>
<textarea style="width:100%;resize:vertical;" id="lora-manager-notes" placeholder="Place for things you want to remember..."></textarea>
<p id="civitai-section" class="displayNone">
<b>Civitai model page:</b>
<a id="civitai-model-page" target="_blank"></a>
</p>
</div>
</div>`
let tabHTML=`
<div id="model-tool-tab-bar" class="tab-container tab-centered">
<span id="tab-model-loraUI" class="tab active">
<span><i class="fa-solid fa-key"></i> Lora Keywords</small></span>
</span>
<span id="tab-model-mergeUI" class="tab">
<span><i class="fa-solid fa-code-merge"></i> Merge Models</small></span>
</span>
</div>
<div id="model-tool-tab-content" class="panel-box">
<div id="tab-content-model-loraUI" class="tab-content active">
<div class="tab-content-inner">
${loraUI}
</div>
</div>
<div id="tab-content-model-mergeUI" class="tab-content">
<div class="tab-content-inner">
${mergeUI}
</div>
</div>
</div>`
///////////////////// Function section
function smoothstep(x) {
return x * x * (3 - 2 * x)
}
function smootherstep(x) {
return x * x * x * (x * (x * 6 - 15) + 10)
}
function smootheststep(x) {
let y = -20 * Math.pow(x, 7)
y += 70 * Math.pow(x, 6)
y -= 84 * Math.pow(x, 5)
y += 35 * Math.pow(x, 4)
return y
}
function getCurrentTime() {
const now = new Date()
let hours = now.getHours()
let minutes = now.getMinutes()
let seconds = now.getSeconds()
hours = hours < 10 ? `0${hours}` : hours
minutes = minutes < 10 ? `0${minutes}` : minutes
seconds = seconds < 10 ? `0${seconds}` : seconds
return `${hours}:${minutes}:${seconds}`
}
function addLogMessage(message) {
const logContainer = document.getElementById("merge-log")
logContainer.innerHTML += `<i>${getCurrentTime()}</i> ${message}<br>`
// Scroll to the bottom of the log
logContainer.scrollTop = logContainer.scrollHeight
document.querySelector("#merge-log-container").style.display = "block"
}
function addLogSeparator() {
const logContainer = document.getElementById("merge-log")
logContainer.innerHTML += "<hr>"
logContainer.scrollTop = logContainer.scrollHeight
}
function drawDiagram(fn) {
const SIZE = 300
const canvas = document.getElementById("merge-canvas")
canvas.height = canvas.width = SIZE
const ctx = canvas.getContext("2d")
// Draw coordinate system
ctx.scale(1, -1)
ctx.translate(0, -canvas.height)
ctx.lineWidth = 1
ctx.beginPath()
ctx.strokeStyle = "white"
ctx.moveTo(0, 0)
ctx.lineTo(0, SIZE)
ctx.lineTo(SIZE, SIZE)
ctx.lineTo(SIZE, 0)
ctx.lineTo(0, 0)
ctx.lineTo(SIZE, SIZE)
ctx.stroke()
ctx.beginPath()
ctx.setLineDash([1, 2])
const n = SIZE / 10
for (let i = n; i < SIZE; i += n) {
ctx.moveTo(0, i)
ctx.lineTo(SIZE, i)
ctx.moveTo(i, 0)
ctx.lineTo(i, SIZE)
}
ctx.stroke()
ctx.beginPath()
ctx.setLineDash([])
ctx.beginPath()
ctx.strokeStyle = "black"
ctx.lineWidth = 3
// Plot function
const numSamples = 20
for (let i = 0; i <= numSamples; i++) {
const x = i / numSamples
const y = fn(x)
const canvasX = x * SIZE
const canvasY = y * SIZE
if (i === 0) {
ctx.moveTo(canvasX, canvasY)
} else {
ctx.lineTo(canvasX, canvasY)
}
}
ctx.stroke()
// Plot alpha values (yellow boxes)
let start = parseFloat(document.querySelector("#merge-start").value)
let step = parseFloat(document.querySelector("#merge-step").value)
let iterations = document.querySelector("#merge-count").value >> 0
ctx.beginPath()
ctx.fillStyle = "yellow"
for (let i = 0; i < iterations; i++) {
const alpha = (start + i * step) / 100
const x = alpha * SIZE
const y = fn(alpha) * SIZE
if (x <= SIZE) {
ctx.rect(x - 3, y - 3, 6, 6)
ctx.fill()
} else {
ctx.strokeStyle = "red"
ctx.moveTo(0, 0)
ctx.lineTo(0, SIZE)
ctx.lineTo(SIZE, SIZE)
ctx.lineTo(SIZE, 0)
ctx.lineTo(0, 0)
ctx.lineTo(SIZE, SIZE)
ctx.stroke()
addLogMessage("<i>Warning: maximum ratio is &#8805; 100%</i>")
}
}
}
function updateChart() {
let fn = (x) => x
switch (document.querySelector("#merge-interpolation").value) {
case "SmoothStep":
fn = smoothstep
break
case "SmootherStep":
fn = smootherstep
break
case "SmoothestStep":
fn = smootheststep
break
}
drawDiagram(fn)
}
function initMergeUI() {
const tabSettingsSingle = document.querySelector("#tab-merge-opts-single")
const tabSettingsBatch = document.querySelector("#tab-merge-opts-batch")
linkTabContents(tabSettingsSingle)
linkTabContents(tabSettingsBatch)
let mergeModelAField = new ModelDropdown(document.querySelector("#mergeModelA"), "stable-diffusion")
let mergeModelBField = new ModelDropdown(document.querySelector("#mergeModelB"), "stable-diffusion")
updateChart()
// slider
const singleMergeRatioField = document.querySelector("#single-merge-ratio")
const singleMergeRatioSlider = document.querySelector("#single-merge-ratio-slider")
function updateSingleMergeRatio() {
singleMergeRatioField.value = singleMergeRatioSlider.value / 10
singleMergeRatioField.dispatchEvent(new Event("change"))
}
function updateSingleMergeRatioSlider() {
if (singleMergeRatioField.value < 0) {
singleMergeRatioField.value = 0
} else if (singleMergeRatioField.value > 100) {
singleMergeRatioField.value = 100
}
singleMergeRatioSlider.value = singleMergeRatioField.value * 10
singleMergeRatioSlider.dispatchEvent(new Event("change"))
}
singleMergeRatioSlider.addEventListener("input", updateSingleMergeRatio)
singleMergeRatioField.addEventListener("input", updateSingleMergeRatioSlider)
updateSingleMergeRatio()
document.querySelector(".merge-config").addEventListener("change", updateChart)
document.querySelector("#merge-button").addEventListener("click", async function(e) {
// Build request template
let model0 = mergeModelAField.value
let model1 = mergeModelBField.value
let request = { model0: model0, model1: model1 }
request["use_fp16"] = document.querySelector("#merge-fp").value == "fp16"
let iterations = document.querySelector("#merge-count").value >> 0
let start = parseFloat(document.querySelector("#merge-start").value)
let step = parseFloat(document.querySelector("#merge-step").value)
if (isTabActive(tabSettingsSingle)) {
start = parseFloat(singleMergeRatioField.value)
step = 0
iterations = 1
addLogMessage(`merge ratio = ${start}%`)
} else {
addLogMessage(`start = ${start}%`)
addLogMessage(`step = ${step}%`)
}
if (start + (iterations - 1) * step >= 100) {
addLogMessage("<i>Aborting: maximum ratio is &#8805; 100%</i>")
addLogMessage("Reduce the number of variations or the step size")
addLogSeparator()
document.querySelector("#merge-count").focus()
return
}
if (document.querySelector("#merge-filename").value == "") {
addLogMessage("<i>Aborting: No output file name specified</i>")
addLogSeparator()
document.querySelector("#merge-filename").focus()
return
}
// Disable merge button
e.target.disabled = true
e.target.classList.add("disabled")
let cursor = $("body").css("cursor")
let label = document.querySelector("#merge-button").innerHTML
$("body").css("cursor", "progress")
document.querySelector("#merge-button").innerHTML = "Merging models ..."
addLogMessage("Merging models")
addLogMessage("Model A: " + model0)
addLogMessage("Model B: " + model1)
// Batch main loop
for (let i = 0; i < iterations; i++) {
let alpha = (start + i * step) / 100
if (isTabActive(tabSettingsBatch)) {
switch (document.querySelector("#merge-interpolation").value) {
case "SmoothStep":
alpha = smoothstep(alpha)
break
case "SmootherStep":
alpha = smootherstep(alpha)
break
case "SmoothestStep":
alpha = smootheststep(alpha)
break
}
}
addLogMessage(`merging batch job ${i + 1}/${iterations}, alpha = ${alpha.toFixed(5)}...`)
request["out_path"] = document.querySelector("#merge-filename").value
request["out_path"] += "-" + alpha.toFixed(5) + "." + document.querySelector("#merge-format").value
addLogMessage(`&nbsp;&nbsp;filename: ${request["out_path"]}`)
// sdkit documentation: "ratio - the ratio of the second model. 1 means only the second model will be used."
request["ratio"] = 1-alpha
let res = await fetch("/model/merge", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
})
const data = await res.json()
addLogMessage(JSON.stringify(data))
}
addLogMessage(
"<b>Done.</b> The models have been saved to your <tt>models/stable-diffusion</tt> folder."
)
addLogSeparator()
// Re-enable merge button
$("body").css("cursor", cursor)
document.querySelector("#merge-button").innerHTML = label
e.target.disabled = false
e.target.classList.remove("disabled")
// Update model list
stableDiffusionModelField.innerHTML = ""
vaeModelField.innerHTML = ""
hypernetworkModelField.innerHTML = ""
await getModels()
})
}
const LoraUI = {
modelField: undefined,
keywordsField: undefined,
notesField: undefined,
civitaiImportBtn: undefined,
civitaiSecion: undefined,
civitaiAnchor: undefined,
image: undefined,
imagePlaceholder: undefined,
init() {
LoraUI.modelField = new ModelDropdown(document.querySelector("#loraModel"), "lora")
LoraUI.keywordsField = document.querySelector("#lora-manager-keywords")
LoraUI.notesField = document.querySelector("#lora-manager-notes")
LoraUI.civitaiImportBtn = document.querySelector("#lora-keyword-from-civitai")
LoraUI.civitaiSection = document.querySelector("#civitai-section")
LoraUI.civitaiAnchor = document.querySelector("#civitai-model-page")
LoraUI.image = document.querySelector("#lora-manager-image")
LoraUI.imagePlaceholder = document.querySelector("#lora-manager-image-placeholder")
LoraUI.uploadBtn = document.querySelector("#lora-manager-upload-button")
LoraUI.uploadInput = document.querySelector("#lora-manager-upload-input")
LoraUI.modelField.addEventListener("change", LoraUI.updateFields)
LoraUI.keywordsField.addEventListener("focusout", LoraUI.saveInfos)
LoraUI.notesField.addEventListener("focusout", LoraUI.saveInfos)
LoraUI.civitaiImportBtn.addEventListener("click", LoraUI.importFromCivitai)
LoraUI.uploadBtn.addEventListener("click", (e) => LoraUI.uploadInput.click())
LoraUI.uploadInput.addEventListener("change", LoraUI.uploadLoraThumb)
document.addEventListener("saveThumb", LoraUI.updateFields)
LoraUI.updateFields()
},
uploadLoraThumb(e) {
console.log(e)
if (LoraUI.uploadInput.files.length === 0) {
return
}
let reader = new FileReader()
let file = LoraUI.uploadInput.files[0]
reader.addEventListener("load", (event) => {
let img = document.createElement("img")
img.src = reader.result
onUseAsThumbnailClick(
{
use_lora_model: LoraUI.modelField.value,
},
img
)
})
if (file) {
reader.readAsDataURL(file)
}
},
updateFields() {
document.getElementById("civitai-section").classList.add("displayNone")
Bucket.retrieve(`modelinfo/lora/${LoraUI.modelField.value}`)
.then((info) => {
if (info == null) {
LoraUI.keywordsField.value = ""
LoraUI.notesField.value = ""
LoraUI.hideCivitaiLink()
} else {
LoraUI.keywordsField.value = info.keywords.join("\n")
LoraUI.notesField.value = info.notes
if ("civitai" in info && info["civitai"] != null) {
LoraUI.showCivitaiLink(info.civitai)
} else {
LoraUI.hideCivitaiLink()
}
}
})
Bucket.getImageAsDataURL(`${profileNameField.value}/lora/${LoraUI.modelField.value}.png`)
.then((data) => {
LoraUI.image.src=data
LoraUI.image.classList.remove("displayNone")
LoraUI.imagePlaceholder.classList.add("displayNone")
})
.catch((error) => {
LoraUI.image.classList.add("displayNone")
LoraUI.imagePlaceholder.classList.remove("displayNone")
})
},
saveInfos() {
let info = {
keywords: LoraUI.keywordsField.value
.split("\n")
.filter((x) => (x != "")),
notes: LoraUI.notesField.value,
civitai: LoraUI.civitaiSection.checkVisibility() ? LoraUI.civitaiAnchor.href : null,
}
Bucket.store(`modelinfo/lora/${LoraUI.modelField.value}`, info)
},
importFromCivitai() {
document.body.style["cursor"] = "progress"
fetch("/sha256/lora/"+LoraUI.modelField.value)
.then((result) => result.json())
.then((json) => fetch("https://civitai.com/api/v1/model-versions/by-hash/" + json.digest))
.then((result) => result.json())
.then((json) => {
document.body.style["cursor"] = "default"
if (json == null) {
return
}
if ("trainedWords" in json) {
LoraUI.keywordsField.value = json["trainedWords"].join("\n")
} else {
showToast("No keyword info found.")
}
if ("modelId" in json) {
LoraUI.showCivitaiLink("https://civitai.com/models/" + json.modelId)
} else {
LoraUI.hideCivitaiLink()
}
LoraUI.saveInfos()
})
},
showCivitaiLink(href) {
LoraUI.civitaiSection.classList.remove("displayNone")
LoraUI.civitaiAnchor.href = href
LoraUI.civitaiAnchor.innerHTML = LoraUI.civitaiAnchor.href
},
hideCivitaiLink() {
LoraUI.civitaiSection.classList.add("displayNone")
}
}
createTab({
id: "merge",
icon: "fa-toolbox",
label: "Model tools",
css: mergeCSS,
content: tabHTML,
onOpen: ({ firstOpen }) => {
if (!firstOpen) {
return
}
initMergeUI()
LoraUI.init()
const tabMergeUI = document.querySelector("#tab-model-mergeUI")
const tabLoraUI = document.querySelector("#tab-model-loraUI")
linkTabContents(tabMergeUI)
linkTabContents(tabLoraUI)
},
})
})()
async function getLoraKeywords(model) {
return Bucket.retrieve(`modelinfo/lora/${model}`)
.then((info) => info ? info.keywords : [])
}