Download web/inputs.mjs from ceroomedia/DMAI-NODES: direct link, hf CLI and curl.
- Browser
- Download file 17.9 kB
-
https://huggingface.co/ceroomedia/DMAI-NODES/resolve/6e0df2b5334898f615431b75e477bab85c5ab136/web/inputs.mjs
- Command line
-
hf download hf://ceroomedia/DMAI-NODES@6e0df2b5334898f615431b75e477bab85c5ab136/web/inputs.mjs
-
curl -L -o inputs.mjs https://huggingface.co/ceroomedia/DMAI-NODES/resolve/6e0df2b5334898f615431b75e477bab85c5ab136/web/inputs.mjs
17.9 kB
| import { | |
| Controller, | |
| el, | |
| button, | |
| input, | |
| details, | |
| statusMessage, | |
| } from "./dom.mjs"; | |
| import { | |
| RATIOS, | |
| dimensions, | |
| customDimensions, | |
| readPrompt, | |
| readManifest, | |
| moveEntry, | |
| uid, | |
| } from "./core.mjs"; | |
| import { ProgressView, progressPresentation } from "./progress-view.mjs"; | |
| export class Prompter extends Controller { | |
| constructor(node, context) { | |
| super(node, "config_json", "Prompter", "text", context); | |
| this.root.classList.add("dmai-prompter"); | |
| const scroll = el("div", "dmai-prompter-scroll"); | |
| scroll.append(...this.root.childNodes); | |
| this.root.append(scroll); | |
| this.progress = { phase: "idle", percent: 0 }; | |
| this.progressView = new ProgressView(this.root); | |
| this.size(360, 400); | |
| this.hydrate(); | |
| this.stopProgress = context.watchProgress?.(node, (state) => { | |
| if (!this.disposed) this.updateProgress(state); | |
| }); | |
| } | |
| hydrate() { | |
| if (this.disposed) return; | |
| try { | |
| this.value = readPrompt(this.native.value); | |
| this.note(); | |
| this.build(); | |
| } catch (error) { | |
| this.invalid(error); | |
| } | |
| } | |
| persist() { | |
| this.set(this.value); | |
| } | |
| build() { | |
| this.body.replaceChildren(); | |
| const prompt = el("textarea", "dmai-prompt"); | |
| prompt.placeholder = "Describe your image…"; | |
| prompt.value = this.value.prompt; | |
| prompt.setAttribute("aria-label", "Image description"); | |
| prompt.addEventListener("input", () => { | |
| this.value.prompt = prompt.value; | |
| this.persist(); | |
| }); | |
| this.body.append(prompt); | |
| this.ratioRow = el("div", "dmai-ratios"); | |
| for (const ratio of ["1:1", "4:5", "9:16", "16:9"]) | |
| this.ratioRow.append(this.ratioButton(ratio)); | |
| this.ratioRow.append( | |
| button("All 19 aspect ratios", "more", () => this.showRatios()), | |
| ); | |
| this.body.append(this.ratioRow); | |
| const resolutionRow = el("div", "dmai-resolution"), | |
| group = el("div", "dmai-segmented"); | |
| this.resolutionButtons = {}; | |
| for (const r of ["1K", "2K", "Custom"]) { | |
| const b = button( | |
| r, | |
| r === "Custom" ? "frame" : null, | |
| () => (r === "Custom" ? this.custom() : this.chooseResolution(r)), | |
| true, | |
| ); | |
| this.resolutionButtons[r] = b; | |
| group.append(b); | |
| } | |
| this.dimensions = el("output", "dmai-dimensions"); | |
| resolutionRow.append(group, this.dimensions); | |
| this.body.append(resolutionRow); | |
| const countRow = el("div", "dmai-row dmai-count"); | |
| countRow.append(el("span", "dmai-muted", "Images")); | |
| this.less = button("One fewer image", "minus", () => this.count(-1)); | |
| this.more = button("One more image", "plus", () => this.count(1)); | |
| this.countOutput = el("output"); | |
| this.countOutput.setAttribute("aria-label", "Image count"); | |
| const stepper = el("div", "dmai-stepper"); | |
| stepper.append(this.less, this.countOutput, this.more); | |
| countRow.append(stepper); | |
| this.body.append(countRow); | |
| const advanced = details(); | |
| const negative = el("textarea", "dmai-negative"); | |
| negative.value = this.value.negative_prompt; | |
| negative.placeholder = "Negative prompt"; | |
| negative.setAttribute("aria-label", "Negative prompt"); | |
| negative.addEventListener("input", () => { | |
| this.value.negative_prompt = negative.value; | |
| this.persist(); | |
| }); | |
| advanced.body.append(negative); | |
| this.body.append(advanced.root); | |
| this.runButton = button( | |
| "Generate", | |
| "play", | |
| () => this.generate(), | |
| true, | |
| ); | |
| this.runButton.classList.add("dmai-primary", "dmai-generate"); | |
| this.body.append(this.runButton, this.progressView.readout); | |
| this.sync(); | |
| this.renderProgress(); | |
| } | |
| updateProgress(state) { | |
| if (this.progress?.phase === "error" && progressPresentation(state).busy) | |
| this.note(); | |
| this.progress = state; | |
| if (state.phase === "error" && state.message) | |
| this.error(new Error(state.message)); | |
| this.renderProgress(); | |
| } | |
| renderProgress() { | |
| const view = this.progressView.update(this.progress); | |
| if (this.runButton) this.runButton.disabled = Boolean(this.queuePending || view.busy); | |
| } | |
| async generate() { | |
| if (this.queuePending || progressPresentation(this.progress).busy || this.disposed) return; | |
| return this.guard(async () => { | |
| this.queuePending = true; | |
| this.note(); | |
| this.renderProgress(); | |
| try { | |
| await this.context.generate(this.node); | |
| } catch (error) { | |
| this.updateProgress({ | |
| phase: "error", | |
| percent: this.progress?.percent ?? 0, | |
| message: error?.message ?? String(error), | |
| }); | |
| throw error; | |
| } finally { | |
| this.queuePending = false; | |
| if (!this.disposed) this.renderProgress(); | |
| } | |
| }); | |
| } | |
| dispose() { | |
| if (this.disposed) return; | |
| this.stopProgress?.(); | |
| this.progressView?.dispose(); | |
| super.dispose(); | |
| } | |
| intent() { | |
| const v = this.node.properties?.dmai_format; | |
| if (v && RATIOS.includes(v.ratio) && ["1K", "2K"].includes(v.resolution)) { | |
| const [w, h] = dimensions(v.ratio, v.resolution); | |
| if (w === this.value.width && h === this.value.height) return v; | |
| } | |
| return { ratio: "4:5", resolution: "Custom" }; | |
| } | |
| remember(ratio, resolution) { | |
| this.node.properties ??= {}; | |
| this.node.properties.dmai_format = { ratio, resolution }; | |
| } | |
| ratioButton(ratio) { | |
| const b = button(ratio, null, () => this.chooseRatio(ratio), true), | |
| [w, h] = ratio.split(":").map(Number), | |
| shape = el("span", "dmai-ratio-shape"); | |
| shape.style.aspectRatio = String(w / h); | |
| b.prepend(shape); | |
| b.dataset.ratio = ratio; | |
| return b; | |
| } | |
| chooseRatio(ratio) { | |
| const intent = this.intent(), | |
| resolution = intent.resolution === "Custom" ? "1K" : intent.resolution; | |
| [this.value.width, this.value.height] = dimensions(ratio, resolution); | |
| this.remember(ratio, resolution); | |
| this.persist(); | |
| this.sync(); | |
| } | |
| chooseResolution(resolution) { | |
| const ratio = this.intent().ratio; | |
| [this.value.width, this.value.height] = dimensions(ratio, resolution); | |
| this.remember(ratio, resolution); | |
| this.persist(); | |
| this.sync(); | |
| } | |
| sync() { | |
| const intent = this.intent(); | |
| for (const b of this.ratioRow.querySelectorAll("[data-ratio]")) | |
| b.setAttribute( | |
| "aria-pressed", | |
| String( | |
| intent.resolution !== "Custom" && intent.ratio === b.dataset.ratio, | |
| ), | |
| ); | |
| for (const [r, b] of Object.entries(this.resolutionButtons)) | |
| b.setAttribute("aria-pressed", String(r === intent.resolution)); | |
| this.dimensions.textContent = `${this.value.width} × ${this.value.height}`; | |
| this.countOutput.textContent = this.value.count; | |
| this.less.disabled = this.value.count <= 1; | |
| this.more.disabled = this.value.count >= 8; | |
| } | |
| count(delta) { | |
| this.value.count = Math.max(1, Math.min(8, this.value.count + delta)); | |
| this.persist(); | |
| this.sync(); | |
| } | |
| showRatios() { | |
| const d = this.show("Aspect ratio"), | |
| grid = el("div", "dmai-format-grid"); | |
| for (const ratio of RATIOS) { | |
| const b = this.ratioButton(ratio); | |
| b.addEventListener("click", () => d.root.close()); | |
| grid.append(b); | |
| } | |
| d.body.append(grid); | |
| } | |
| custom() { | |
| const d = this.show("Custom resolution"), | |
| grid = el("div", "dmai-custom-size"), | |
| w = input("Width", "number", this.value.width), | |
| h = input("Height", "number", this.value.height), | |
| out = el("output", "dmai-size-output"), | |
| note = el("p", "dmai-muted"), | |
| apply = button( | |
| "Apply size", | |
| "check", | |
| () => { | |
| const values = read(); | |
| if (!values) return; | |
| [this.value.width, this.value.height] = values; | |
| this.remember(this.intent().ratio, "Custom"); | |
| this.persist(); | |
| this.sync(); | |
| d.root.close(); | |
| }, | |
| true, | |
| ); | |
| apply.classList.add("dmai-primary"); | |
| for (const field of [w, h]) { | |
| field.control.min = 64; | |
| field.control.max = 8192; | |
| field.control.step = 1; | |
| field.control.addEventListener("input", read); | |
| } | |
| grid.append( | |
| w.field, | |
| button("Swap dimensions", "swap", () => { | |
| [w.control.value, h.control.value] = [h.control.value, w.control.value]; | |
| read(); | |
| }), | |
| h.field, | |
| ); | |
| d.body.append(grid, out, note, apply); | |
| function read() { | |
| try { | |
| const values = customDimensions( | |
| w.control.value === "" ? NaN : Number(w.control.value), | |
| h.control.value === "" ? NaN : Number(h.control.value), | |
| ); | |
| out.textContent = `${values[0]} × ${values[1]} px`; | |
| note.textContent = | |
| "Rounded to the nearest 64 px. The engine checks model limits."; | |
| apply.disabled = false; | |
| return values; | |
| } catch (error) { | |
| out.textContent = ""; | |
| note.textContent = error.message; | |
| apply.disabled = true; | |
| return null; | |
| } | |
| } | |
| read(); | |
| } | |
| } | |
| export class LoRAStack extends Controller { | |
| constructor(node, context) { | |
| super(node, "manifest_json", "LoRA Loader", "layers", context); | |
| const serialize = this.native.serializeValue?.bind(this.native); | |
| this.native.serializeValue = (...args) => { | |
| if (!this.syncStrengths()) | |
| throw new Error( | |
| "Correct the highlighted LoRA strength before queueing.", | |
| ); | |
| readManifest(this.native.value); | |
| return serialize ? serialize(...args) : this.native.value; | |
| }; | |
| this.size(390, 275); | |
| this.hydrate(); | |
| } | |
| hydrate() { | |
| if (this.disposed) return; | |
| try { | |
| this.value = readManifest(this.native.value); | |
| this.note(); | |
| this.build(); | |
| } catch (error) { | |
| this.invalid(error); | |
| } | |
| } | |
| persist() { | |
| this.set(this.value); | |
| } | |
| updateStrength(id, text) { | |
| const value = text.trim() === "" ? NaN : Number(text); | |
| if (!Number.isFinite(value) || value < -20 || value > 20) { | |
| this.error( | |
| new Error( | |
| "Strength must be from −20 to 20. Correct it before queueing.", | |
| ), | |
| ); | |
| return false; | |
| } | |
| const entry = this.value.entries.find((item) => item.id === id); | |
| if (!entry) return false; | |
| entry.strength_model = value; | |
| this.persist(); | |
| this.note(); | |
| return true; | |
| } | |
| syncStrengths() { | |
| for (const control of this.body.querySelectorAll(".dmai-strength")) { | |
| if (!this.updateStrength(control.dataset.entryId, control.value)) { | |
| control.setAttribute("aria-invalid", "true"); | |
| control.focus(); | |
| return false; | |
| } | |
| control.removeAttribute("aria-invalid"); | |
| } | |
| return true; | |
| } | |
| move(id, to) { | |
| if (!this.syncStrengths()) return; | |
| this.value.entries = moveEntry(this.value.entries, id, to); | |
| this.persist(); | |
| this.build(); | |
| const moved = this.value.entries.find((e) => e.id === id); | |
| this.note( | |
| `${moved.name} · position ${this.value.entries.indexOf(moved) + 1}`, | |
| ); | |
| this.body | |
| .querySelector(`[data-entry-id="${CSS.escape(id)}"] .dmai-grip`) | |
| ?.focus(); | |
| } | |
| build() { | |
| this.body.replaceChildren(); | |
| const list = el("div", "dmai-lora-list"); | |
| list.setAttribute("aria-label", "Ordered LoRA stack"); | |
| let dragged = null; | |
| this.value.entries.forEach((entry, index) => { | |
| const row = el("div", `dmai-lora-row${entry.enabled ? "" : " dmai-off"}`); | |
| row.dataset.entryId = entry.id; | |
| const grip = button(`Reorder ${entry.name}; Alt and arrow keys`, "grip"); | |
| grip.classList.add("dmai-grip"); | |
| grip.draggable = true; | |
| grip.addEventListener("keydown", (event) => { | |
| if (event.altKey && ["ArrowUp", "ArrowDown"].includes(event.key)) { | |
| event.preventDefault(); | |
| this.move(entry.id, index + (event.key === "ArrowUp" ? -1 : 1)); | |
| } | |
| }); | |
| grip.addEventListener("dragstart", (event) => { | |
| dragged = entry.id; | |
| event.dataTransfer.effectAllowed = "move"; | |
| event.dataTransfer.setData("text/plain", entry.id); | |
| row.classList.add("dmai-dragging"); | |
| }); | |
| grip.addEventListener("dragend", () => { | |
| dragged = null; | |
| row.classList.remove("dmai-dragging"); | |
| list | |
| .querySelectorAll(".dmai-dragover") | |
| .forEach((r) => r.classList.remove("dmai-dragover")); | |
| }); | |
| row.addEventListener("dragover", (event) => { | |
| if (dragged) { | |
| event.preventDefault(); | |
| row.classList.add("dmai-dragover"); | |
| } | |
| }); | |
| row.addEventListener("dragleave", () => | |
| row.classList.remove("dmai-dragover"), | |
| ); | |
| row.addEventListener("drop", (event) => { | |
| event.preventDefault(); | |
| if (dragged) this.move(dragged, index); | |
| dragged = null; | |
| }); | |
| const copy = el("div", "dmai-lora-copy"), | |
| title = el("span", "dmai-lora-name", entry.name.split(/[\\/]/).pop()); | |
| title.title = entry.name; | |
| copy.append( | |
| title, | |
| el( | |
| "span", | |
| "dmai-muted", | |
| `${String(index + 1).padStart(2, "0")} · CLIP ${entry.strength_clip}`, | |
| ), | |
| ); | |
| const strength = el("input", "dmai-strength"); | |
| strength.type = "number"; | |
| strength.min = -20; | |
| strength.max = 20; | |
| strength.step = 0.05; | |
| strength.value = entry.strength_model; | |
| strength.dataset.entryId = entry.id; | |
| strength.setAttribute("aria-label", `Model strength for ${entry.name}`); | |
| const commitStrength = () => { | |
| if (this.updateStrength(entry.id, strength.value)) | |
| strength.removeAttribute("aria-invalid"); | |
| else strength.setAttribute("aria-invalid", "true"); | |
| }; | |
| strength.addEventListener("input", commitStrength); | |
| strength.addEventListener("change", commitStrength); | |
| const toggle = button( | |
| `${entry.enabled ? "Disable" : "Enable"} ${entry.name}`, | |
| "power", | |
| () => { | |
| if (!this.syncStrengths()) return; | |
| entry.enabled = !entry.enabled; | |
| this.persist(); | |
| this.build(); | |
| }, | |
| ); | |
| toggle.setAttribute("aria-pressed", String(entry.enabled)); | |
| const arrows = el("div", "dmai-reorder"); | |
| const up = button(`Move ${entry.name} up`, "up", () => | |
| this.move(entry.id, index - 1), | |
| ), | |
| down = button(`Move ${entry.name} down`, "down", () => | |
| this.move(entry.id, index + 1), | |
| ); | |
| up.disabled = index === 0; | |
| down.disabled = index === this.value.entries.length - 1; | |
| arrows.append(up, down); | |
| const edit = button(`Edit ${entry.name}`, "sliders", () => | |
| this.edit(entry), | |
| ); | |
| row.append(grip, copy, strength, toggle, arrows, edit); | |
| list.append(row); | |
| }); | |
| if (!this.value.entries.length) | |
| list.append(el("div", "dmai-empty", "Add a LoRA to build your stack.")); | |
| this.body.append( | |
| list, | |
| button("Add LoRA", "plus", () => this.guard(() => this.library()), true), | |
| ); | |
| const count = this.value.entries.filter( | |
| (e) => e.enabled && (e.strength_model !== 0 || e.strength_clip !== 0), | |
| ).length; | |
| this.body.append( | |
| el("div", "dmai-footnote", `${count} selected · top to bottom`), | |
| ); | |
| } | |
| edit(entry) { | |
| if (!this.syncStrengths()) return; | |
| const d = this.show("LoRA settings"), | |
| field = input("CLIP strength", "number", entry.strength_clip); | |
| field.control.min = -20; | |
| field.control.max = 20; | |
| field.control.step = 0.05; | |
| const message = el("p", "dmai-status"), | |
| apply = button( | |
| "Apply", | |
| "check", | |
| () => { | |
| const n = | |
| field.control.value === "" ? NaN : Number(field.control.value); | |
| if (!Number.isFinite(n) || n < -20 || n > 20) { | |
| statusMessage( | |
| message, | |
| "CLIP strength must be from −20 to 20.", | |
| true, | |
| ); | |
| return; | |
| } | |
| entry.strength_clip = n; | |
| this.persist(); | |
| this.build(); | |
| d.root.close(); | |
| }, | |
| true, | |
| ), | |
| remove = button( | |
| "Remove LoRA", | |
| "close", | |
| () => { | |
| this.value.entries = this.value.entries.filter( | |
| (e) => e.id !== entry.id, | |
| ); | |
| this.persist(); | |
| this.build(); | |
| d.root.close(); | |
| }, | |
| true, | |
| ); | |
| d.body.append( | |
| el("p", "dmai-filename", entry.name), | |
| field.field, | |
| message, | |
| apply, | |
| remove, | |
| ); | |
| } | |
| async library() { | |
| if (!this.syncStrengths()) return; | |
| const d = this.show("LoRA library"), | |
| search = input("Search LoRAs", "search"), | |
| results = el("div", "dmai-library"), | |
| message = el("p", "dmai-muted", "Loading LoRAs…"); | |
| d.body.append(search.field, results, message); | |
| try { | |
| const data = await this.context.bootstrap(true); | |
| if (!d.root.isConnected) return; | |
| const draw = () => { | |
| results.replaceChildren(); | |
| const entries = data.loras.filter((e) => | |
| e.name.toLowerCase().includes(search.control.value.toLowerCase()), | |
| ); | |
| for (const item of entries) { | |
| const b = button( | |
| item.name, | |
| "plus", | |
| () => { | |
| this.value.entries.push({ | |
| id: uid(), | |
| name: item.name, | |
| enabled: true, | |
| strength_model: 1, | |
| strength_clip: 1, | |
| }); | |
| this.persist(); | |
| this.build(); | |
| d.root.close(); | |
| }, | |
| true, | |
| ); | |
| results.append(b); | |
| } | |
| message.textContent = entries.length | |
| ? `${entries.length} available` | |
| : "No matching LoRAs. Add files to ComfyUI/models/loras and refresh."; | |
| }; | |
| search.control.addEventListener("input", draw); | |
| draw(); | |
| } catch (error) { | |
| message.textContent = error.message; | |
| message.dataset.error = "true"; | |
| } | |
| } | |
| } | |