mirror of
https://github.com/easydiffusion/easydiffusion.git
synced 2024-11-22 08:13:22 +01:00
Auto-save LoRAs
This commit is contained in:
parent
83de2b8de7
commit
afd879a692
@ -332,8 +332,7 @@
|
||||
<label for="lora_model">LoRA:</label>
|
||||
</td>
|
||||
<td class="diffusers-restart-needed">
|
||||
<div class="model_entries"></div>
|
||||
<button class="add_model_entry"><i class="fa-solid fa-plus"></i> add another LoRA</button>
|
||||
<div id="lora_model" data-path=""></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="hypernetwork_model_container" class="pl-5"><td><label for="hypernetwork_model">Hypernetwork:</label></td><td>
|
||||
@ -788,6 +787,7 @@
|
||||
<script src="media/js/auto-save.js"></script>
|
||||
|
||||
<script src="media/js/searchable-models.js"></script>
|
||||
<script src="media/js/multi-model-selector.js"></script>
|
||||
<script src="media/js/main.js"></script>
|
||||
<script src="media/js/plugins.js"></script>
|
||||
<script src="media/js/themes.js"></script>
|
||||
|
@ -57,6 +57,7 @@ const SETTINGS_IDS_LIST = [
|
||||
"json_toggle",
|
||||
"extract_lora_from_prompt",
|
||||
"embedding-card-size-selector",
|
||||
"lora_model",
|
||||
]
|
||||
|
||||
const IGNORE_BY_DEFAULT = ["prompt"]
|
||||
|
@ -121,6 +121,7 @@ let clipSkipField = document.querySelector("#clip_skip")
|
||||
let tilingField = document.querySelector("#tiling")
|
||||
let controlnetModelField = new ModelDropdown(document.querySelector("#controlnet_model"), "controlnet", "None", false)
|
||||
let vaeModelField = new ModelDropdown(document.querySelector("#vae_model"), "vae", "None")
|
||||
let loraModelField = new MultiModelSelector(document.querySelector("#lora_model"), "lora", "LoRA", 0.5, 0.02)
|
||||
let hypernetworkModelField = new ModelDropdown(document.querySelector("#hypernetwork_model"), "hypernetwork", "None")
|
||||
let hypernetworkStrengthSlider = document.querySelector("#hypernetwork_strength_slider")
|
||||
let hypernetworkStrengthField = document.querySelector("#hypernetwork_strength")
|
||||
@ -2928,76 +2929,6 @@ prettifyInputs(document)
|
||||
promptField.focus()
|
||||
promptField.selectionStart = promptField.value.length
|
||||
|
||||
// multi-models
|
||||
let modelCount = 0
|
||||
|
||||
function addModelEntry(modelContainer, modelsList, modelType, defaultValue, strengthStep) {
|
||||
let idx = modelCount++
|
||||
let nameId = modelType + "_model_" + idx
|
||||
let strengthId = modelType + "_alpha_" + idx
|
||||
|
||||
const modelElement = document.createElement("div")
|
||||
modelElement.className = "model_entry"
|
||||
modelElement.innerHTML = `
|
||||
<input id="${nameId}" class="model_name" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
|
||||
<input id="${strengthId}" class="model_strength" type="number" step="${strengthStep}" style="width: 50pt" value="${defaultValue}" pattern="^-?[0-9]*\.?[0-9]*$" onkeypress="preventNonNumericalInput(event)">
|
||||
`
|
||||
modelContainer.appendChild(modelElement)
|
||||
|
||||
let modelName = new ModelDropdown(modelElement.querySelector(".model_name"), modelType, "None")
|
||||
let modelStrength = modelElement.querySelector(".model_strength")
|
||||
let entry = [modelName, modelStrength, modelElement]
|
||||
|
||||
let removeBtn = document.createElement("button")
|
||||
removeBtn.className = "remove_model_btn"
|
||||
removeBtn.setAttribute("title", "Remove model")
|
||||
removeBtn.innerHTML = '<i class="fa-solid fa-minus"></i>'
|
||||
|
||||
if (modelsList.length === 0) {
|
||||
removeBtn.classList.add("displayNone")
|
||||
}
|
||||
|
||||
removeBtn.addEventListener("click", function() {
|
||||
let entryIdx = modelsList.indexOf(entry)
|
||||
modelsList.splice(entryIdx, 1)
|
||||
modelContainer.removeChild(modelElement)
|
||||
})
|
||||
|
||||
modelElement.appendChild(removeBtn)
|
||||
|
||||
modelsList.push(entry)
|
||||
|
||||
return modelElement
|
||||
}
|
||||
|
||||
function createLoraEntry() {
|
||||
let container = document.querySelector("#lora_model_container .model_entries")
|
||||
return addModelEntry(container, loraModels, "lora", 0.5, 0.02)
|
||||
}
|
||||
|
||||
function createLoraEntries() {
|
||||
let firstEntry = createLoraEntry()
|
||||
|
||||
let addLoraBtn = document.querySelector("#lora_model_container .add_model_entry")
|
||||
addLoraBtn.addEventListener("click", () => {
|
||||
createLoraEntry()
|
||||
})
|
||||
}
|
||||
createLoraEntries()
|
||||
|
||||
// chrome-like spinners only on hover
|
||||
// function showSpinnerOnlyOnHover(e) {
|
||||
// e.addEventListener("mouseenter", () => {
|
||||
// e.setAttribute("type", "number")
|
||||
// })
|
||||
// e.addEventListener("mouseleave", () => {
|
||||
// e.removeAttribute("type")
|
||||
// })
|
||||
// e.removeAttribute("type")
|
||||
// }
|
||||
|
||||
// document.querySelectorAll("input[type=number]").forEach(showSpinnerOnlyOnHover)
|
||||
|
||||
////////////////////////////// Image Size Widget //////////////////////////////////////////
|
||||
|
||||
function roundToMultiple(number, n) {
|
||||
|
218
ui/media/js/multi-model-selector.js
Normal file
218
ui/media/js/multi-model-selector.js
Normal file
@ -0,0 +1,218 @@
|
||||
/**
|
||||
* A component consisting of multiple model dropdowns, along with a "weight" field per model.
|
||||
*
|
||||
* Behaves like a single input element, giving an object in response to the .value field.
|
||||
*
|
||||
* Inspired by the design of the ModelDropdown component (searchable-models.js).
|
||||
*/
|
||||
|
||||
class MultiModelSelector {
|
||||
root
|
||||
modelType
|
||||
modelNameFriendly
|
||||
defaultWeight
|
||||
weightStep
|
||||
|
||||
modelContainer
|
||||
addNewButton
|
||||
|
||||
counter = 0
|
||||
|
||||
/* MIMIC A REGULAR INPUT FIELD */
|
||||
get id() {
|
||||
return this.root.id
|
||||
}
|
||||
get parentElement() {
|
||||
return this.root.parentElement
|
||||
}
|
||||
get parentNode() {
|
||||
return this.root.parentNode
|
||||
}
|
||||
get value() {
|
||||
let modelNames = []
|
||||
let modelWeights = []
|
||||
|
||||
this.modelElements.forEach((e) => {
|
||||
modelNames.push(e.name.value)
|
||||
modelWeights.push(e.weight.value)
|
||||
})
|
||||
|
||||
return { modelNames: modelNames, modelWeights: modelWeights }
|
||||
}
|
||||
set value(modelData) {
|
||||
if (typeof modelData !== "object") {
|
||||
throw new Error("Multi-model selector expects an object containing modelNames and modelWeights as keys!")
|
||||
}
|
||||
if (!("modelNames" in modelData) || !("modelWeights" in modelData)) {
|
||||
throw new Error("modelNames or modelWeights not present in the data passed to the multi-model selector")
|
||||
}
|
||||
|
||||
let newModelNames = modelData["modelNames"]
|
||||
let newModelWeights = modelData["modelWeights"]
|
||||
if (newModelNames.length !== newModelWeights.length) {
|
||||
throw new Error("Need to pass an equal number of modelNames and modelWeights!")
|
||||
}
|
||||
|
||||
// expand or shrink entries
|
||||
let currElements = this.modelElements
|
||||
if (currElements.length < newModelNames.length) {
|
||||
for (let i = currElements.length; i < newModelNames.length; i++) {
|
||||
this.addModelEntry()
|
||||
}
|
||||
} else {
|
||||
for (let i = newModelNames.length; i < currElements.length; i++) {
|
||||
this.removeModelEntry()
|
||||
}
|
||||
}
|
||||
|
||||
// assign to the corresponding elements
|
||||
currElements = this.modelElements
|
||||
for (let i = 0; i < newModelNames.length; i++) {
|
||||
let curr = currElements[i]
|
||||
|
||||
// update weight first, name second.
|
||||
// for some unholy reason this order matters for dispatch chains
|
||||
// the root of all this unholiness is because searchable-models automatically dispatches an update event
|
||||
// as soon as the value is updated via JS, which is against the DOM pattern of not dispatching an event automatically
|
||||
// unless the caller explicitly dispatches the event.
|
||||
curr.weight.value = newModelWeights[i]
|
||||
curr.name.value = newModelNames[i]
|
||||
}
|
||||
}
|
||||
get disabled() {
|
||||
return false
|
||||
}
|
||||
set disabled(state) {
|
||||
// do nothing
|
||||
}
|
||||
get modelElements() {
|
||||
let entries = this.root.querySelectorAll(".model_entry")
|
||||
entries = [...entries]
|
||||
let elements = entries.map((e) => {
|
||||
return { name: e.querySelector(".model_name").field, weight: e.querySelector(".model_weight") }
|
||||
})
|
||||
return elements
|
||||
}
|
||||
addEventListener(type, listener, options) {
|
||||
// do nothing
|
||||
}
|
||||
dispatchEvent(event) {
|
||||
// do nothing
|
||||
}
|
||||
appendChild(option) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
// remember 'this' - http://blog.niftysnippets.org/2008/04/you-must-remember-this.html
|
||||
bind(f, obj) {
|
||||
return function() {
|
||||
return f.apply(obj, arguments)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(root, modelType, modelNameFriendly = undefined, defaultWeight = 0.5, weightStep = 0.02) {
|
||||
this.root = root
|
||||
this.modelType = modelType
|
||||
this.modelNameFriendly = modelNameFriendly || modelType
|
||||
this.defaultWeight = defaultWeight
|
||||
this.weightStep = weightStep
|
||||
|
||||
let self = this
|
||||
document.addEventListener("refreshModels", function() {
|
||||
setTimeout(self.bind(self.populateModels, self), 1)
|
||||
})
|
||||
|
||||
this.createStructure()
|
||||
this.populateModels()
|
||||
}
|
||||
|
||||
createStructure() {
|
||||
this.modelContainer = document.createElement("div")
|
||||
this.modelContainer.className = "model_entries"
|
||||
this.root.appendChild(this.modelContainer)
|
||||
|
||||
this.addNewButton = document.createElement("button")
|
||||
this.addNewButton.className = "add_model_entry"
|
||||
this.addNewButton.innerHTML = '<i class="fa-solid fa-plus"></i> add another ' + this.modelNameFriendly
|
||||
this.addNewButton.addEventListener("click", this.bind(this.addModelEntry, this))
|
||||
this.root.appendChild(this.addNewButton)
|
||||
}
|
||||
|
||||
populateModels() {
|
||||
if (this.root.dataset.path === "") {
|
||||
if (this.length === 0) {
|
||||
this.addModelEntry() // create a single blank entry
|
||||
}
|
||||
} else {
|
||||
this.value = JSON.parse(this.root.dataset.path)
|
||||
}
|
||||
}
|
||||
|
||||
addModelEntry() {
|
||||
let idx = this.counter++
|
||||
let currLength = this.length
|
||||
|
||||
const modelElement = document.createElement("div")
|
||||
modelElement.className = "model_entry"
|
||||
modelElement.innerHTML = `
|
||||
<input id="${this.modelType}_${idx}" class="model_name model-filter" type="text" spellcheck="false" autocomplete="off" data-path="" />
|
||||
<input class="model_weight" type="number" step="${this.weightStep}" style="width: 50pt" value="${this.defaultWeight}" pattern="^-?[0-9]*\.?[0-9]*$" onkeypress="preventNonNumericalInput(event)">
|
||||
`
|
||||
this.modelContainer.appendChild(modelElement)
|
||||
|
||||
let modelNameEl = modelElement.querySelector(".model_name")
|
||||
modelNameEl.field = new ModelDropdown(modelNameEl, this.modelType, "None")
|
||||
let modelWeightEl = modelElement.querySelector(".model_weight")
|
||||
|
||||
let self = this
|
||||
|
||||
function makeUpdateEvent(type) {
|
||||
return function(e) {
|
||||
e.stopPropagation()
|
||||
|
||||
let modelData = self.value
|
||||
self.root.dataset.path = JSON.stringify(modelData)
|
||||
|
||||
self.root.dispatchEvent(new Event(type))
|
||||
}
|
||||
}
|
||||
|
||||
modelNameEl.addEventListener("change", makeUpdateEvent("change"))
|
||||
modelNameEl.addEventListener("input", makeUpdateEvent("input"))
|
||||
modelWeightEl.addEventListener("change", makeUpdateEvent("change"))
|
||||
modelWeightEl.addEventListener("input", makeUpdateEvent("input"))
|
||||
|
||||
let removeBtn = document.createElement("button")
|
||||
removeBtn.className = "remove_model_btn"
|
||||
removeBtn.setAttribute("title", "Remove model")
|
||||
removeBtn.innerHTML = '<i class="fa-solid fa-minus"></i>'
|
||||
|
||||
if (currLength === 0) {
|
||||
removeBtn.classList.add("displayNone")
|
||||
}
|
||||
|
||||
removeBtn.addEventListener(
|
||||
"click",
|
||||
this.bind(function(e) {
|
||||
this.modelContainer.removeChild(modelElement)
|
||||
|
||||
makeUpdateEvent("change")(e)
|
||||
}, this)
|
||||
)
|
||||
|
||||
modelElement.appendChild(removeBtn)
|
||||
}
|
||||
|
||||
removeModelEntry() {
|
||||
if (this.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let lastEntry = this.modelContainer.lastElementChild
|
||||
lastEntry.remove()
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this.modelContainer.childElementCount
|
||||
}
|
||||
}
|
@ -118,14 +118,17 @@ class ModelDropdown {
|
||||
)
|
||||
}
|
||||
|
||||
saveCurrentSelection(elem, value, path) {
|
||||
saveCurrentSelection(elem, value, path, dispatchEvent = true) {
|
||||
this.currentSelection.elem = elem
|
||||
this.currentSelection.value = value
|
||||
this.currentSelection.path = path
|
||||
this.modelFilter.dataset.path = path
|
||||
this.modelFilter.value = value
|
||||
|
||||
if (dispatchEvent) {
|
||||
this.modelFilter.dispatchEvent(new Event("change"))
|
||||
}
|
||||
}
|
||||
|
||||
processClick(e) {
|
||||
e.preventDefault()
|
||||
@ -348,13 +351,13 @@ class ModelDropdown {
|
||||
}
|
||||
}
|
||||
|
||||
selectEntry(path) {
|
||||
selectEntry(path, dispatchEvent = true) {
|
||||
if (path !== undefined) {
|
||||
const entries = this.modelElements
|
||||
|
||||
for (const elem of entries) {
|
||||
if (elem.dataset.path == path) {
|
||||
this.saveCurrentSelection(elem, elem.innerText, elem.dataset.path)
|
||||
this.saveCurrentSelection(elem, elem.innerText, elem.dataset.path, dispatchEvent)
|
||||
this.highlightedModelEntry = elem
|
||||
elem.scrollIntoView({ block: "nearest" })
|
||||
break
|
||||
@ -529,7 +532,7 @@ class ModelDropdown {
|
||||
rootModelList.style.minWidth = modelFilterStyle.width
|
||||
})
|
||||
|
||||
this.selectEntry(this.activeModel)
|
||||
this.selectEntry(this.activeModel, false)
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user