Efficiency Nodes V2.0

This commit is contained in:
TSC
2023-10-20 15:49:32 -05:00
committed by GitHub
parent 749c42b69b
commit 93eb925686
41 changed files with 5013 additions and 888 deletions

View File

@@ -0,0 +1,104 @@
.pysssss-model-info {
color: white;
font-family: sans-serif;
max-width: 90vw;
}
.pysssss-model-content {
display: flex;
flex-direction: column;
overflow: hidden;
}
.pysssss-model-info h2 {
text-align: center;
margin: 0 0 10px 0;
}
.pysssss-model-info p {
margin: 5px 0;
}
.pysssss-model-info a {
color: dodgerblue;
}
.pysssss-model-info a:hover {
text-decoration: underline;
}
.pysssss-model-tags-list {
display: flex;
flex-wrap: wrap;
list-style: none;
gap: 10px;
max-height: 200px;
overflow: auto;
margin: 10px 0;
padding: 0;
}
.pysssss-model-tag {
background-color: rgb(128, 213, 247);
color: #000;
display: flex;
align-items: center;
gap: 5px;
border-radius: 5px;
padding: 2px 5px;
cursor: pointer;
}
.pysssss-model-tag--selected span::before {
content: "✅";
position: absolute;
background-color: dodgerblue;
left: 0;
top: 0;
right: 0;
bottom: 0;
text-align: center;
}
.pysssss-model-tag:hover {
outline: 2px solid dodgerblue;
}
.pysssss-model-tag p {
margin: 0;
}
.pysssss-model-tag span {
text-align: center;
border-radius: 5px;
background-color: dodgerblue;
color: #fff;
padding: 2px;
position: relative;
min-width: 20px;
overflow: hidden;
}
.pysssss-model-metadata .comfy-modal-content {
max-width: 100%;
}
.pysssss-model-metadata label {
margin-right: 1ch;
color: #ccc;
}
.pysssss-model-metadata span {
color: dodgerblue;
}
.pysssss-preview {
max-width: 50%;
margin-left: 10px;
position: relative;
}
.pysssss-preview img {
max-height: 300px;
}
.pysssss-preview button {
position: absolute;
font-size: 12px;
bottom: 10px;
right: 10px;
}
.pysssss-model-notes {
background-color: rgba(0, 0, 0, 0.25);
padding: 5px;
margin-top: 5px;
}
.pysssss-model-notes:empty {
display: none;
}

View File

@@ -0,0 +1,303 @@
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: metadata[k] })])
)
)
);
}
}
export class ModelInfoDialog extends ComfyDialog {
constructor(name) {
super();
this.name = name;
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));
} 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() {
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",
})
);
if (info.images?.length) {
this.img.src = info.images[0].url;
this.img.style.display = "";
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();
},
});
}
return info;
})
.catch((err) => {
content.textContent = "⚠️ " + err.message;
});
}
}

View File

@@ -0,0 +1,94 @@
import { app } from '../../../../scripts/app.js'
import { $el } from "../../../../scripts/ui.js";
export function addStylesheet(url) {
if (url.endsWith(".js")) {
url = url.substr(0, url.length - 2) + "css";
}
$el("link", {
parent: document.head,
rel: "stylesheet",
type: "text/css",
href: url.startsWith("http") ? url : getUrl(url),
});
}
export function getUrl(path, baseUrl) {
if (baseUrl) {
return new URL(path, baseUrl).toString();
} else {
return new URL("../" + path, import.meta.url).toString();
}
}
export async function loadImage(url) {
return new Promise((res, rej) => {
const img = new Image();
img.onload = res;
img.onerror = rej;
img.src = url;
});
}
export function addMenuHandler(nodeType, cb) {
const GROUPED_MENU_ORDER = {
"🔄 Swap with...": 0,
"⛓ Add link...": 1,
"📜 Add script...": 2,
"🔍 View model info...": 3,
"🌱 Seed behavior...": 4,
"📐 Set Resolution...": 5,
"✏️ Add 𝚇 input...": 6,
"✏️ Add 𝚈 input...": 7
};
const originalGetOpts = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function () {
let r = originalGetOpts ? originalGetOpts.apply(this, arguments) || [] : [];
const insertOption = (option) => {
if (GROUPED_MENU_ORDER.hasOwnProperty(option.content)) {
// Find the right position for the option
let targetPos = r.length; // default to the end
for (let i = 0; i < r.length; i++) {
if (GROUPED_MENU_ORDER.hasOwnProperty(r[i].content) &&
GROUPED_MENU_ORDER[option.content] < GROUPED_MENU_ORDER[r[i].content]) {
targetPos = i;
break;
}
}
// Insert the option at the determined position
r.splice(targetPos, 0, option);
} else {
// If the option is not in the GROUPED_MENU_ORDER, simply add it to the end
r.push(option);
}
};
cb.call(this, insertOption);
return r;
};
}
export function findWidgetByName(node, widgetName) {
return node.widgets.find(widget => widget.name === widgetName);
}
// Utility functions
export function addNode(name, nextTo, options) {
options = { select: true, shiftX: 0, shiftY: 0, before: false, ...(options || {}) };
const node = LiteGraph.createNode(name);
app.graph.add(node);
node.pos = [
nextTo.pos[0] + options.shiftX,
nextTo.pos[1] + options.shiftY,
];
if (options.select) {
app.canvas.selectNode(node, false);
}
return node;
}