Spaces:
Running
on
Zero
Running
on
Zero
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); | |
}; | |
}, | |
}); | |