import { app } from "../../../scripts/app.js"; import { api } from "../../../scripts/api.js"; import { $el } from "../../../scripts/ui.js"; import { ModelInfoDialog } from "./common/modelInfoDialog.js"; const MAX_TAGS = 500; const NsfwLevel = { PG: 1, PG13: 2, R: 4, X: 8, XXX: 16, Blocked: 32, }; export class LoraInfoDialog extends ModelInfoDialog { getTagFrequency() { if (!this.metadata.ss_tag_frequency) return []; const datasets = JSON.parse(this.metadata.ss_tag_frequency); const tags = {}; for (const setName in datasets) { const set = datasets[setName]; for (const t in set) { if (t in tags) { tags[t] += set[t]; } else { tags[t] = set[t]; } } } return Object.entries(tags).sort((a, b) => b[1] - a[1]); } getResolutions() { let res = []; if (this.metadata.ss_bucket_info) { const parsed = JSON.parse(this.metadata.ss_bucket_info); if (parsed?.buckets) { for (const { resolution, count } of Object.values(parsed.buckets)) { res.push([count, `${resolution.join("x")} * ${count}`]); } } } res = res.sort((a, b) => b[0] - a[0]).map((a) => a[1]); let r = this.metadata.ss_resolution; if (r) { const s = r.split(","); const w = s[0].replace("(", ""); const h = s[1].replace(")", ""); res.push(`${w.trim()}x${h.trim()} (Base res)`); } else if ((r = this.metadata["modelspec.resolution"])) { res.push(r + " (Base res"); } if (!res.length) { res.push("⚠️ Unknown"); } return res; } getTagList(tags) { return tags.map((t) => $el( "li.pysssss-model-tag", { dataset: { tag: t[0], }, $: (el) => { el.onclick = () => { el.classList.toggle("pysssss-model-tag--selected"); }; }, }, [ $el("p", { textContent: t[0], }), $el("span", { textContent: t[1], }), ] ) ); } addTags() { let tags = this.getTagFrequency(); if (!tags?.length) { tags = this.metadata["modelspec.tags"]?.split(",").map((t) => [t.trim(), 1]); } let hasMore; if (tags?.length) { const c = tags.length; let list; if (c > MAX_TAGS) { tags = tags.slice(0, MAX_TAGS); hasMore = $el("p", [ $el("span", { textContent: `⚠️ Only showing first ${MAX_TAGS} tags ` }), $el("a", { href: "#", textContent: `Show all ${c}`, onclick: () => { list.replaceChildren(...this.getTagList(this.getTagFrequency())); hasMore.remove(); }, }), ]); } list = $el("ol.pysssss-model-tags-list", this.getTagList(tags)); this.tags = $el("div", [list]); } else { this.tags = $el("p", { textContent: "⚠️ No tag frequency metadata found" }); } this.content.append(this.tags); if (hasMore) { this.content.append(hasMore); } } addExample(title, value, name) { const textArea = $el("textarea", { textContent: value, style: { whiteSpace: "pre-wrap", margin: "10px 0", color: "#fff", background: "#222", padding: "5px", borderRadius: "5px", maxHeight: "250px", overflow: "auto", display: "block", border: "none", width: "calc(100% - 10px)", }, }); $el( "p", { parent: this.content, textContent: `${title}: `, }, [ textArea, $el("button", { onclick: async () => { await this.saveAsExample(textArea.value, `${name}.txt`); }, textContent: "Save as Example", style: { fontSize: "14px", }, }), $el("hr"), ] ); } async addInfo() { this.addInfoEntry("Name", this.metadata.ss_output_name || "⚠️ Unknown"); this.addInfoEntry("Base Model", this.metadata.ss_sd_model_name || "⚠️ Unknown"); this.addInfoEntry("Clip Skip", this.metadata.ss_clip_skip || "⚠️ Unknown"); this.addInfoEntry( "Resolution", $el( "select", this.getResolutions().map((r) => $el("option", { textContent: r })) ) ); super.addInfo(); const p = this.addCivitaiInfo(); this.addTags(); const info = await p; this.addExample("Trained Words", info?.trainedWords?.join(", ") ?? "", "trainedwords"); const triggerPhrase = this.metadata["modelspec.trigger_phrase"]; if (triggerPhrase) { this.addExample("Trigger Phrase", triggerPhrase, "triggerphrase"); } $el("div", { parent: this.content, innerHTML: info?.description ?? this.metadata["modelspec.description"] ?? "[No description provided]", style: { maxHeight: "250px", overflow: "auto", }, }); } async saveAsExample(example, name = "example.txt") { if (!example.length) { return; } try { name = prompt("Enter example name", name); if (!name) return; await api.fetchApi("/pysssss/examples/" + encodeURIComponent(`${this.type}/${this.name}`), { method: "POST", body: JSON.stringify({ name, example, }), headers: { "content-type": "application/json", }, }); this.node?.["pysssss.updateExamples"]?.(); alert("Saved!"); } catch (error) { console.error(error); alert("Error saving: " + error); } } createButtons() { const btns = super.createButtons(); function tagsToCsv(tags) { return tags.map((el) => el.dataset.tag).join(", "); } function copyTags(e, tags) { const textarea = $el("textarea", { parent: document.body, style: { position: "fixed", }, textContent: tagsToCsv(tags), }); textarea.select(); try { document.execCommand("copy"); if (!e.target.dataset.text) { e.target.dataset.text = e.target.textContent; } e.target.textContent = "Copied " + tags.length + " tags"; setTimeout(() => { e.target.textContent = e.target.dataset.text; }, 1000); } catch (ex) { prompt("Copy to clipboard: Ctrl+C, Enter", text); } finally { document.body.removeChild(textarea); } } btns.unshift( $el("button", { type: "button", textContent: "Save Selected as Example", onclick: async (e) => { const tags = tagsToCsv([...this.tags.querySelectorAll(".pysssss-model-tag--selected")]); await this.saveAsExample(tags); }, }), $el("button", { type: "button", textContent: "Copy Selected", onclick: (e) => { copyTags(e, [...this.tags.querySelectorAll(".pysssss-model-tag--selected")]); }, }), $el("button", { type: "button", textContent: "Copy All", onclick: (e) => { copyTags(e, [...this.tags.querySelectorAll(".pysssss-model-tag")]); }, }) ); return btns; } } class CheckpointInfoDialog extends ModelInfoDialog { async addInfo() { super.addInfo(); const info = await this.addCivitaiInfo(); if (info) { this.addInfoEntry("Base Model", info.baseModel || "⚠️ Unknown"); $el("div", { parent: this.content, innerHTML: info.description, style: { maxHeight: "250px", overflow: "auto", }, }); } } } const lookups = {}; function addInfoOption(node, type, infoClass, widgetNamePattern, opts) { const widgets = widgetNamePattern ? node.widgets.filter((w) => w.name === widgetNamePattern || w.name.match(`^${widgetNamePattern}$`)) : [node.widgets[0]]; for (const widget of widgets) { let value = widget.value; if (value?.content) { value = value.content; } if (!value || value === "None") { return; } let optName; const split = value.split(/[.\\/]/); optName = split[split.length - 2]; opts.push({ content: optName, callback: async () => { new infoClass(value, node).show(type, value); }, }); } } function addTypeOptions(node, typeName, options) { const type = typeName.toLowerCase() + "s"; const values = lookups[typeName][node.type]; if (!values) return; const widgets = Object.keys(values); const cls = type === "loras" ? LoraInfoDialog : CheckpointInfoDialog; const opts = []; for (const w of widgets) { addInfoOption(node, type, cls, w, opts); } if (!opts.length) return; if (opts.length === 1) { opts[0].content = `View ${typeName} info...`; options.unshift(opts[0]); } else { options.unshift({ title: `View ${typeName} info...`, has_submenu: true, submenu: { options: opts, }, }); } } app.registerExtension({ name: "pysssss.ModelInfo", setup() { const addSetting = (type, defaultValue) => { app.ui.settings.addSetting({ id: `pysssss.ModelInfo.${type}Nodes`, name: `🐍 Model Info - ${type} Nodes/Widgets`, type: "text", defaultValue, tooltip: `Comma separated list of NodeTypeName or NodeTypeName.WidgetName that contain ${type} node names that should have the View Info option available.\nIf no widget name is specifed the first widget will be used. Regex matches (e.g. NodeName..*lora_\\d+) are supported in the widget name.`, onChange(value) { lookups[type] = value.split(",").reduce((p, n) => { n = n.trim(); const pos = n.indexOf("."); const split = pos === -1 ? [n] : [n.substring(0, pos), n.substring(pos + 1)]; p[split[0]] ??= {}; p[split[0]][split[1] ?? ""] = true; return p; }, {}); }, }); }; addSetting( "Lora", ["LoraLoader.lora_name", "LoraLoader|pysssss", "LoraLoaderModelOnly.lora_name", "LoRA Stacker.lora_name.*"].join(",") ); addSetting( "Checkpoint", ["CheckpointLoader.ckpt_name", "CheckpointLoaderSimple", "CheckpointLoader|pysssss", "Efficient Loader", "Eff. Loader SDXL"].join(",") ); app.ui.settings.addSetting({ id: `pysssss.ModelInfo.NsfwLevel`, name: `🐍 Model Info - Image Preview Max NSFW Level`, type: "combo", defaultValue: "PG13", options: Object.keys(NsfwLevel), tooltip: `Hides preview images that are tagged as a higher NSFW level`, onChange(value) { ModelInfoDialog.nsfwLevel = NsfwLevel[value] ?? NsfwLevel.PG; }, }); }, beforeRegisterNodeDef(nodeType) { const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; nodeType.prototype.getExtraMenuOptions = function (_, options) { if (this.widgets) { for (const type in lookups) { addTypeOptions(this, type, options); } } return getExtraMenuOptions?.apply(this, arguments); }; }, });