diff --git a/ui/easydiffusion/server.py b/ui/easydiffusion/server.py index 311b3a03..5ca4f452 100644 --- a/ui/easydiffusion/server.py +++ b/ui/easydiffusion/server.py @@ -470,7 +470,6 @@ def modify_package_internal(package_name: str, req: dict): def get_sha256_internal(obj_path): - import hashlib from easydiffusion.utils import sha256sum path = obj_path.split("/") diff --git a/ui/easydiffusion/utils/__init__.py b/ui/easydiffusion/utils/__init__.py index f6758809..a930725b 100644 --- a/ui/easydiffusion/utils/__init__.py +++ b/ui/easydiffusion/utils/__init__.py @@ -1,4 +1,5 @@ import logging +import hashlib log = logging.getLogger("easydiffusion") diff --git a/ui/media/js/main.js b/ui/media/js/main.js index 744568ec..7634f288 100644 --- a/ui/media/js/main.js +++ b/ui/media/js/main.js @@ -511,10 +511,10 @@ function showImages(reqBody, res, outputContainer, livePreview) { { text: "Upscale", on_click: onUpscaleClick }, { text: "Fix Faces", on_click: onFixFacesClick }, ], - { + { text: "Use as Thumbnail", 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) } - let embeddings = req.use_embeddings_model.map((e) => e.split("/").pop()) - let LORA = [] + useAsThumbSelect.innerHTML="" - if ("use_lora_model" in req) { - LORA = req.use_lora_model + if ("use_embeddings_model" in req) { + 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() onUseAsThumbnailClick.scale = scale } @@ -782,6 +802,50 @@ useAsThumbCancelBtn.addEventListener("click", () => { 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) => { let scale = 1 / onUseAsThumbnailClick.scale let crop = onUseAsThumbnailClick.croppr.getValue() @@ -793,16 +857,11 @@ useAsThumbSaveBtn.addEventListener("click", (e) => { .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, - }) + Bucket.upload(`${profileName}/${embedding.dataset["type"]}/${embedding.value}.png`, blob) ) } return Promise.all(promises) @@ -2362,20 +2421,10 @@ function loadThumbnailImageFromFile() { } function updateEmbeddingsList(filter = "") { - function html(model, iconlist = [], prefix = "", filter = "") { + function html(model, iconMap = {}, prefix = "", filter = "") { filter = filter.toLowerCase() 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) => { @@ -2383,13 +2432,9 @@ function updateEmbeddingsList(filter = "") { 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]}` + if (token in iconMap) { + img = `/bucket/${profileName}/${iconMap[token]}` } button = createModifierCard(m, [img, img], true) // } @@ -2398,7 +2443,7 @@ function updateEmbeddingsList(filter = "") { toplevel.appendChild(button) } } 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") { let div1 = document.createElement("div") let div2 = document.createElement("div") @@ -2457,11 +2502,44 @@ function updateEmbeddingsList(filter = "") { ` + let loraTokens = [] let profileName = profileNameField.value - fetch(`/bucket/${profileName}/embeddings/`) - .then((response) => (response.status == 200 ? response.json() : [])) - .then(async function(iconlist) { - embeddingsList.replaceChildren(html(modelsOptions.embeddings, iconlist, "", filter)) + let iconMap = {} + + Bucket.getList(`${profileName}/embeddings/`) + .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) if (filter != "") { embeddingsExpandAll() diff --git a/ui/plugins/ui/merge.plugin.js b/ui/plugins/ui/merge.plugin.js index d3ddedbf..639afc07 100644 --- a/ui/plugins/ui/merge.plugin.js +++ b/ui/plugins/ui/merge.plugin.js @@ -1,6 +1,337 @@ ;(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 = ` +
+
+

+ +

+ +

+

Important: Please merge models of similar type.
For e.g. SD 1.4 models with only SD 1.4/1.5 models,
SD 2.0 with SD 2.0-type, and SD 2.1 with SD 2.1-type models.

+
+ + + + + + + + + + + + + +
Base name of the output file.
Mix ratio and file suffix will be appended to this.
+ Image generation uses fp16, so it's a good choice.
Use fp32 if you want to use the result models for more mixes
+
+
+
+
+

+
+
+
+
+
+ + Make a single file + + + Make multiple variations + +
+
+
+
+ Saves a single merged model file, at the specified merge ratio.

+ + + % + Model A's contribution to the mix. The rest will be from Model B. +
+
+
+
+ Saves multiple variations of the model, at different merge ratios.
Each variation will be saved as a separate file.


+ + + + + + + + + + + + + +
Number of models to create
% Smallest share of model A in the mix
% Share of model A added into the mix per step
Sigmoid function to be applied to the model share before mixing
+
+ Preview of variation ratios:
+ +
+
+
+
+
+ +
+
` + + + let loraUI=` +
+
+ + +
+
+

Thumbnail:

+
+ + +
+
+ + +
+
+
+

Keywords: +

+ +

+ LORA model keywords can be used via the + Embeddings button. They get added to the embedding + keyword menu when the LORA has been selected in the image settings. +

+
+
+

Notes:

+ +

+ Civitai model page: + +

+
+
` + + let tabHTML=` +
+ + Lora Keywords + + + Merge Models + +
+
+
+
+ ${loraUI} +
+
+ +
+
+ ${mergeUI} +
+
+
` + + ///////////////////// Function section function smoothstep(x) { return x * x * (3 - 2 * x) @@ -140,315 +471,266 @@ } 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; + + 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")) } - .merge-input { - grid-area: merge-input; - padding-left:1em; + 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")) } - .merge-config { - grid-area: merge-config; - padding:1em; + + 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("Aborting: maximum ratio is ≥ 100%") + addLogMessage("Reduce the number of variations or the step size") + addLogSeparator() + document.querySelector("#merge-count").focus() + return + } + + if (document.querySelector("#merge-filename").value == "") { + addLogMessage("Aborting: No output file name specified") + 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(`  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( + "Done. The models have been saved to your models/stable-diffusion 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.modelField.addEventListener("change", LoraUI.updateFields) + LoraUI.keywordsField.addEventListener("focusout", LoraUI.saveInfos) + LoraUI.notesField.addEventListener("focusout", LoraUI.saveInfos) + LoraUI.civitaiImportBtn.addEventListener("click", LoraUI.importFromCivitai) + + LoraUI.updateFields() + }, + + 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) { + LoraUI.showCivitaiLink(info.civitai) + } + } + }) + 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) => { + console.error("Caught error:", 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") } - .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: ` -
-
-

- -

- -

-

Important: Please merge models of similar type.
For e.g. SD 1.4 models with only SD 1.4/1.5 models,
SD 2.0 with SD 2.0-type, and SD 2.1 with SD 2.1-type models.

-
- - - - - - - - - - - - - -
Base name of the output file.
Mix ratio and file suffix will be appended to this.
- Image generation uses fp16, so it's a good choice.
Use fp32 if you want to use the result models for more mixes
-
-
-
-
-

-
-
-
-
-
- - Make a single file - - - Make multiple variations - -
-
-
-
- Saves a single merged model file, at the specified merge ratio.

- - - % - Model A's contribution to the mix. The rest will be from Model B. -
-
-
-
- Saves multiple variations of the model, at different merge ratios.
Each variation will be saved as a separate file.


- - - - - - - - - - - - - -
Number of models to create
% Smallest share of model A in the mix
% Share of model A added into the mix per step
Sigmoid function to be applied to the model share before mixing
-
- Preview of variation ratios:
- -
-
-
-
-
- -
-
`, + } + + createTab({ + id: "merge", + icon: "fa-toolbox", + label: "Model tools", + css: mergeCSS, + content: tabHTML, 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("Aborting: maximum ratio is ≥ 100%") - addLogMessage("Reduce the number of variations or the step size") - addLogSeparator() - document.querySelector("#merge-count").focus() - return - } - - if (document.querySelector("#merge-filename").value == "") { - addLogMessage("Aborting: No output file name specified") - 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(`  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( - "Done. The models have been saved to your models/stable-diffusion 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() - }) + 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.keywords) +}