From d6a02a31a72c324199b772b089d760cd7514f8cc Mon Sep 17 00:00:00 2001 From: JeLuF Date: Sun, 17 Sep 2023 22:36:50 +0200 Subject: [PATCH 1/2] LoraManager: Implement 'Upload thumbnail' button --- ui/media/js/main.js | 1 + ui/plugins/ui/model-tools.plugin.js | 770 ++++++++++++++++++++++++++++ 2 files changed, 771 insertions(+) create mode 100644 ui/plugins/ui/model-tools.plugin.js diff --git a/ui/media/js/main.js b/ui/media/js/main.js index 7634f288..101a88e7 100644 --- a/ui/media/js/main.js +++ b/ui/media/js/main.js @@ -868,6 +868,7 @@ useAsThumbSaveBtn.addEventListener("click", (e) => { }) .then(() => { useAsThumbDialog.close() + document.dispatchEvent(new CustomEvent("saveThumb", { detail: useAsThumbSelect.selectedOptions })) }) .catch((error) => { console.error(error) diff --git a/ui/plugins/ui/model-tools.plugin.js b/ui/plugins/ui/model-tools.plugin.js new file mode 100644 index 00000000..09fd6499 --- /dev/null +++ b/ui/plugins/ui/model-tools.plugin.js @@ -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 = ` +
+
+

+ +

+ +

+

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) + } + + 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 += `${getCurrentTime()} ${message}
` + + // 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 += "
" + + 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("Warning: maximum ratio is ≥ 100%") + } + } + } + + 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("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.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 : []) +} From 14dbebbc35eb3075bc12188f84e5ef80401df1ec Mon Sep 17 00:00:00 2001 From: JeLuF Date: Sun, 17 Sep 2023 22:37:17 +0200 Subject: [PATCH 2/2] LoraManager: Implement 'Upload thumbnail' button --- ui/plugins/ui/merge.plugin.js | 737 ---------------------------------- 1 file changed, 737 deletions(-) delete mode 100644 ui/plugins/ui/merge.plugin.js diff --git a/ui/plugins/ui/merge.plugin.js b/ui/plugins/ui/merge.plugin.js deleted file mode 100644 index 8e74cf61..00000000 --- a/ui/plugins/ui/merge.plugin.js +++ /dev/null @@ -1,737 +0,0 @@ -;(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) - } - - 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 += `${getCurrentTime()} ${message}
` - - // 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 += "
" - - 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("Warning: maximum ratio is ≥ 100%") - } - } - } - - 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("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 && 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 : []) -}