Spaces:
Paused
Paused
| import { $el, ComfyDialog } from "../../../../scripts/ui.js"; | |
| import { api } from "../../../../scripts/api.js"; | |
| import { addStylesheet } from "./utils.js"; | |
| addStylesheet(import.meta.url); | |
| class MetadataDialog extends ComfyDialog { | |
| constructor() { | |
| super(); | |
| this.element.classList.add("pysssss-model-metadata"); | |
| } | |
| show(metadata) { | |
| super.show( | |
| $el( | |
| "div", | |
| Object.keys(metadata).map((k) => | |
| $el("div", [ | |
| $el("label", { textContent: k }), | |
| $el("span", { textContent: typeof metadata[k] === "object" ? JSON.stringify(metadata[k]) : metadata[k] }), | |
| ]) | |
| ) | |
| ) | |
| ); | |
| } | |
| } | |
| export class ModelInfoDialog extends ComfyDialog { | |
| constructor(name, node) { | |
| super(); | |
| this.name = name; | |
| this.node = node; | |
| this.element.classList.add("pysssss-model-info"); | |
| } | |
| get customNotes() { | |
| return this.metadata["pysssss.notes"]; | |
| } | |
| set customNotes(v) { | |
| this.metadata["pysssss.notes"] = v; | |
| } | |
| get hash() { | |
| return this.metadata["pysssss.sha256"]; | |
| } | |
| async show(type, value) { | |
| this.type = type; | |
| const req = api.fetchApi("/pysssss/metadata/" + encodeURIComponent(`${type}/${value}`)); | |
| this.info = $el("div", { style: { flex: "auto" } }); | |
| this.img = $el("img", { style: { display: "none" } }); | |
| this.imgWrapper = $el("div.pysssss-preview", [this.img]); | |
| this.main = $el("main", { style: { display: "flex" } }, [this.info, this.imgWrapper]); | |
| this.content = $el("div.pysssss-model-content", [$el("h2", { textContent: this.name }), this.main]); | |
| const loading = $el("div", { textContent: "ℹ️ Loading...", parent: this.content }); | |
| super.show(this.content); | |
| this.metadata = await (await req).json(); | |
| this.viewMetadata.style.cursor = this.viewMetadata.style.opacity = ""; | |
| this.viewMetadata.removeAttribute("disabled"); | |
| loading.remove(); | |
| this.addInfo(); | |
| } | |
| createButtons() { | |
| const btns = super.createButtons(); | |
| this.viewMetadata = $el("button", { | |
| type: "button", | |
| textContent: "View raw metadata", | |
| disabled: "disabled", | |
| style: { | |
| opacity: 0.5, | |
| cursor: "not-allowed", | |
| }, | |
| onclick: (e) => { | |
| if (this.metadata) { | |
| new MetadataDialog().show(this.metadata); | |
| } | |
| }, | |
| }); | |
| btns.unshift(this.viewMetadata); | |
| return btns; | |
| } | |
| getNoteInfo() { | |
| function parseNote() { | |
| if (!this.customNotes) return []; | |
| let notes = []; | |
| // Extract links from notes | |
| const r = new RegExp("(\\bhttps?:\\/\\/[^\\s]+)", "g"); | |
| let end = 0; | |
| let m; | |
| do { | |
| m = r.exec(this.customNotes); | |
| let pos; | |
| let fin = 0; | |
| if (m) { | |
| pos = m.index; | |
| fin = m.index + m[0].length; | |
| } else { | |
| pos = this.customNotes.length; | |
| } | |
| let pre = this.customNotes.substring(end, pos); | |
| if (pre) { | |
| pre = pre.replaceAll("\n", "<br>"); | |
| notes.push( | |
| $el("span", { | |
| innerHTML: pre, | |
| }) | |
| ); | |
| } | |
| if (m) { | |
| notes.push( | |
| $el("a", { | |
| href: m[0], | |
| textContent: m[0], | |
| target: "_blank", | |
| }) | |
| ); | |
| } | |
| end = fin; | |
| } while (m); | |
| return notes; | |
| } | |
| let textarea; | |
| let notesContainer; | |
| const editText = "✏️ Edit"; | |
| const edit = $el("a", { | |
| textContent: editText, | |
| href: "#", | |
| style: { | |
| float: "right", | |
| color: "greenyellow", | |
| textDecoration: "none", | |
| }, | |
| onclick: async (e) => { | |
| e.preventDefault(); | |
| if (textarea) { | |
| this.customNotes = textarea.value; | |
| const resp = await api.fetchApi("/pysssss/metadata/notes/" + encodeURIComponent(`${this.type}/${this.name}`), { | |
| method: "POST", | |
| body: this.customNotes, | |
| }); | |
| if (resp.status !== 200) { | |
| console.error(resp); | |
| alert(`Error saving notes (${req.status}) ${req.statusText}`); | |
| return; | |
| } | |
| e.target.textContent = editText; | |
| textarea.remove(); | |
| textarea = null; | |
| notesContainer.replaceChildren(...parseNote.call(this)); | |
| this.node?.["pysssss.updateExamples"]?.(); | |
| } else { | |
| e.target.textContent = "💾 Save"; | |
| textarea = $el("textarea", { | |
| style: { | |
| width: "100%", | |
| minWidth: "200px", | |
| minHeight: "50px", | |
| }, | |
| textContent: this.customNotes, | |
| }); | |
| e.target.after(textarea); | |
| notesContainer.replaceChildren(); | |
| textarea.style.height = Math.min(textarea.scrollHeight, 300) + "px"; | |
| } | |
| }, | |
| }); | |
| notesContainer = $el("div.pysssss-model-notes", parseNote.call(this)); | |
| return $el( | |
| "div", | |
| { | |
| style: { display: "contents" }, | |
| }, | |
| [edit, notesContainer] | |
| ); | |
| } | |
| addInfo() { | |
| const usageHint = this.metadata["modelspec.usage_hint"]; | |
| if (usageHint) { | |
| this.addInfoEntry("Usage Hint", usageHint); | |
| } | |
| this.addInfoEntry("Notes", this.getNoteInfo()); | |
| } | |
| addInfoEntry(name, value) { | |
| return $el( | |
| "p", | |
| { | |
| parent: this.info, | |
| }, | |
| [ | |
| typeof name === "string" ? $el("label", { textContent: name + ": " }) : name, | |
| typeof value === "string" ? $el("span", { textContent: value }) : value, | |
| ] | |
| ); | |
| } | |
| async getCivitaiDetails() { | |
| const req = await fetch("https://civitai.com/api/v1/model-versions/by-hash/" + this.hash); | |
| if (req.status === 200) { | |
| return await req.json(); | |
| } else if (req.status === 404) { | |
| throw new Error("Model not found"); | |
| } else { | |
| throw new Error(`Error loading info (${req.status}) ${req.statusText}`); | |
| } | |
| } | |
| addCivitaiInfo() { | |
| const promise = this.getCivitaiDetails(); | |
| const content = $el("span", { textContent: "ℹ️ Loading..." }); | |
| this.addInfoEntry( | |
| $el("label", [ | |
| $el("img", { | |
| style: { | |
| width: "18px", | |
| position: "relative", | |
| top: "3px", | |
| margin: "0 5px 0 0", | |
| }, | |
| src: "https://civitai.com/favicon.ico", | |
| }), | |
| $el("span", { textContent: "Civitai: " }), | |
| ]), | |
| content | |
| ); | |
| return promise | |
| .then((info) => { | |
| content.replaceChildren( | |
| $el("a", { | |
| href: "https://civitai.com/models/" + info.modelId, | |
| textContent: "View " + info.model.name, | |
| target: "_blank", | |
| }) | |
| ); | |
| const allPreviews = info.images?.filter((i) => i.type === "image"); | |
| const previews = allPreviews?.filter((i) => i.nsfwLevel <= ModelInfoDialog.nsfwLevel); | |
| if (previews?.length) { | |
| let previewIndex = 0; | |
| let preview; | |
| const updatePreview = () => { | |
| preview = previews[previewIndex]; | |
| this.img.src = preview.url; | |
| }; | |
| updatePreview(); | |
| this.img.style.display = ""; | |
| this.img.title = `${previews.length} previews.`; | |
| if (allPreviews.length !== previews.length) { | |
| this.img.title += ` ${allPreviews.length - previews.length} images hidden due to NSFW level.`; | |
| } | |
| this.imgSave = $el("button", { | |
| textContent: "Use as preview", | |
| parent: this.imgWrapper, | |
| onclick: async () => { | |
| // Convert the preview to a blob | |
| const blob = await (await fetch(this.img.src)).blob(); | |
| // Store it in temp | |
| const name = "temp_preview." + new URL(this.img.src).pathname.split(".")[1]; | |
| const body = new FormData(); | |
| body.append("image", new File([blob], name)); | |
| body.append("overwrite", "true"); | |
| body.append("type", "temp"); | |
| const resp = await api.fetchApi("/upload/image", { | |
| method: "POST", | |
| body, | |
| }); | |
| if (resp.status !== 200) { | |
| console.error(resp); | |
| alert(`Error saving preview (${req.status}) ${req.statusText}`); | |
| return; | |
| } | |
| // Use as preview | |
| await api.fetchApi("/pysssss/save/" + encodeURIComponent(`${this.type}/${this.name}`), { | |
| method: "POST", | |
| body: JSON.stringify({ | |
| filename: name, | |
| type: "temp", | |
| }), | |
| headers: { | |
| "content-type": "application/json", | |
| }, | |
| }); | |
| app.refreshComboInNodes(); | |
| }, | |
| }); | |
| $el("button", { | |
| textContent: "Show metadata", | |
| parent: this.imgWrapper, | |
| onclick: async () => { | |
| if (preview.meta && Object.keys(preview.meta).length) { | |
| new MetadataDialog().show(preview.meta); | |
| } else { | |
| alert("No image metadata found"); | |
| } | |
| }, | |
| }); | |
| const addNavButton = (icon, direction) => { | |
| $el("button.pysssss-preview-nav", { | |
| textContent: icon, | |
| parent: this.imgWrapper, | |
| onclick: async () => { | |
| previewIndex += direction; | |
| if (previewIndex < 0) { | |
| previewIndex = previews.length - 1; | |
| } else if (previewIndex >= previews.length) { | |
| previewIndex = 0; | |
| } | |
| updatePreview(); | |
| }, | |
| }); | |
| }; | |
| if (previews.length > 1) { | |
| addNavButton("‹", -1); | |
| addNavButton("›", 1); | |
| } | |
| } else if (info.images?.length) { | |
| $el("span", { style: { opacity: 0.6 }, textContent: "⚠️ All images hidden due to NSFW level setting.", parent: this.imgWrapper }); | |
| } | |
| return info; | |
| }) | |
| .catch((err) => { | |
| content.textContent = "⚠️ " + err.message; | |
| }); | |
| } | |
| } | |