add:COMOSITION of preset in easy ipadapterApply
This commit is contained in:
@@ -33,6 +33,7 @@
|
||||
|
||||
**v1.1.3 (2024/4/4)**
|
||||
|
||||
- `easy ipadapterApply` Added **COMPOSITION** preset
|
||||
- Supported [ResAdapter](https://huggingface.co/jiaxiangc/res-adapter) when load ResAdapter lora
|
||||
- Added `easy promptLine`
|
||||
- Added `easy promptReplace`
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
|
||||
**v1.1.3 (2024/4/4)**
|
||||
|
||||
- `easy ipadapterApply` 增加 **COMPOSITION** 预置项
|
||||
- 增加 对[ResAdapter](https://huggingface.co/jiaxiangc/res-adapter) lora模型 的加载支持
|
||||
- 增加 `easy promptLine`
|
||||
- 增加 `easy promptReplace`
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import re
|
||||
import os
|
||||
import torch
|
||||
import hashlib
|
||||
import sys
|
||||
import json
|
||||
import folder_paths
|
||||
from server import PromptServer
|
||||
from .config import RESOURCES_DIR, FOOOCUS_STYLES_DIR, FOOOCUS_STYLES_SAMPLES
|
||||
from .easyNodes import easyCache
|
||||
from .logic import ConvertAnything
|
||||
from .libs.model import easyModelManager
|
||||
from .libs.utils import getMetadata
|
||||
|
||||
try:
|
||||
import aiohttp
|
||||
@@ -129,5 +128,63 @@ async def getModelsThumbnail(request):
|
||||
loras_full.append(full_path)
|
||||
return web.json_response(checkpoints_full + loras_full)
|
||||
|
||||
@PromptServer.instance.routes.get("/easyuse/metadata/{name}")
|
||||
async def load_metadata(request):
|
||||
name = request.match_info["name"]
|
||||
pos = name.index("/")
|
||||
type = name[0:pos]
|
||||
name = name[pos+1:]
|
||||
|
||||
file_path = None
|
||||
if type == "embeddings":
|
||||
name = name.lower()
|
||||
files = folder_paths.get_filename_list(type)
|
||||
for f in files:
|
||||
lower_f = f.lower()
|
||||
if lower_f == name:
|
||||
file_path = folder_paths.get_full_path(type, f)
|
||||
else:
|
||||
n = os.path.splitext(f)[0].lower()
|
||||
if n == name:
|
||||
file_path = folder_paths.get_full_path(type, f)
|
||||
|
||||
if file_path is not None:
|
||||
break
|
||||
else:
|
||||
file_path = folder_paths.get_full_path(type, name)
|
||||
if not file_path:
|
||||
return web.Response(status=404)
|
||||
|
||||
try:
|
||||
header = getMetadata(file_path)
|
||||
header_json = json.loads(header)
|
||||
meta = header_json["__metadata__"] if "__metadata__" in header_json else None
|
||||
except:
|
||||
meta = None
|
||||
|
||||
if meta is None:
|
||||
meta = {}
|
||||
|
||||
file_no_ext = os.path.splitext(file_path)[0]
|
||||
|
||||
info_file = file_no_ext + ".txt"
|
||||
if os.path.isfile(info_file):
|
||||
with open(info_file, "r") as f:
|
||||
meta["easyuse.notes"] = f.read()
|
||||
|
||||
hash_file = file_no_ext + ".sha256"
|
||||
if os.path.isfile(hash_file):
|
||||
with open(hash_file, "rt") as f:
|
||||
meta["easyuse.sha256"] = f.read()
|
||||
else:
|
||||
with open(file_path, "rb") as f:
|
||||
meta["easyuse.sha256"] = hashlib.sha256(f.read()).hexdigest()
|
||||
with open(hash_file, "wt") as f:
|
||||
f.write(meta["easyuse.sha256"])
|
||||
|
||||
return web.json_response(meta)
|
||||
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
@@ -231,6 +231,14 @@ IPADAPTER_MODELS = {
|
||||
"sdxl": {
|
||||
"model_url": "https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid-portrait_sdxl.bin",
|
||||
}
|
||||
},
|
||||
"COMPOSITION": {
|
||||
"sd15": {
|
||||
"model_url": "https://huggingface.co/ostris/ip-composition-adapter/resolve/main/ip_plus_composition_sd15.safetensors"
|
||||
},
|
||||
"sdxl": {
|
||||
"model_url": "https://huggingface.co/ostris/ip-composition-adapter/resolve/main/ip_plus_composition_sdxl.safetensors"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-1
@@ -2029,7 +2029,8 @@ class ipadapter:
|
||||
'VIT-G (medium strength)',
|
||||
'PLUS (high strength)',
|
||||
'PLUS FACE (portraits)',
|
||||
'FULL FACE - SD1.5 only (portraits stronger)'
|
||||
'FULL FACE - SD1.5 only (portraits stronger)',
|
||||
'COMPOSITION'
|
||||
]
|
||||
self.faceid_presets = [
|
||||
'FACEID',
|
||||
@@ -2099,6 +2100,11 @@ class ipadapter:
|
||||
if is_sdxl:
|
||||
raise Exception("full face model is not supported for SDXL")
|
||||
pattern = 'full.face.sd15\.(safetensors|bin)$'
|
||||
elif preset.startswith("composition"):
|
||||
if is_sdxl:
|
||||
pattern = 'plus.composition.sdxl\.(safetensors|bin)$'
|
||||
else:
|
||||
pattern = 'plus.composition.sd15\.(safetensors|bin)$'
|
||||
elif preset.startswith("faceid portrait"):
|
||||
if is_sdxl:
|
||||
pattern = 'portrait.sdxl\.(safetensors|bin)$'
|
||||
|
||||
+22
-2
@@ -121,11 +121,13 @@ def is_linked_styles_selector(prompt, my_unique_id, prompt_type='positive'):
|
||||
else:
|
||||
return False
|
||||
|
||||
use_mirror = False
|
||||
def get_local_filepath(url, dirname, local_file_name=None):
|
||||
"""Get local file path when is already downloaded or download it"""
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
from torch.hub import download_url_to_file
|
||||
global use_mirror
|
||||
if not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
if not local_file_name:
|
||||
@@ -133,13 +135,16 @@ def get_local_filepath(url, dirname, local_file_name=None):
|
||||
local_file_name = os.path.basename(parsed_url.path)
|
||||
destination = os.path.join(dirname, local_file_name)
|
||||
if not os.path.exists(destination):
|
||||
print(f'downloading {url} to {destination}')
|
||||
try:
|
||||
if use_mirror:
|
||||
url = url.replace('huggingface.co', 'hf-mirror.com')
|
||||
print(f'downloading {url} to {destination}')
|
||||
download_url_to_file(url, destination)
|
||||
except Exception as e:
|
||||
args = e.args[0] if e.args is not None and len(e.args) > 0 else None
|
||||
if isinstance(args, TimeoutError):
|
||||
print(f'无法从 {url} 下载,正在尝试使用镜像地址...')
|
||||
use_mirror = True
|
||||
url = url.replace('huggingface.co', 'hf-mirror.com')
|
||||
try:
|
||||
download_url_to_file(url, destination)
|
||||
@@ -177,4 +182,19 @@ def easySave(images, filename_prefix, output_type, prompt=None, extra_pnginfo=No
|
||||
return results['ui']['images']
|
||||
else:
|
||||
results = SaveImage().save_images(images, filename_prefix, prompt, extra_pnginfo)
|
||||
return results['ui']['images']
|
||||
return results['ui']['images']
|
||||
|
||||
def getMetadata(filepath):
|
||||
with open(filepath, "rb") as file:
|
||||
# https://github.com/huggingface/safetensors#format
|
||||
# 8 bytes: N, an unsigned little-endian 64-bit integer, containing the size of the header
|
||||
header_size = int.from_bytes(file.read(8), "little", signed=False)
|
||||
|
||||
if header_size <= 0:
|
||||
raise BufferError("Invalid header size")
|
||||
|
||||
header = file.read(header_size)
|
||||
if header_size <= 0:
|
||||
raise BufferError("Invalid header")
|
||||
|
||||
return header
|
||||
+2
-1
@@ -2,4 +2,5 @@
|
||||
@import "dropdown.css";
|
||||
@import "selector.css";
|
||||
@import "groupmap.css";
|
||||
@import "contextmenu.css";
|
||||
@import "contextmenu.css";
|
||||
@import "modelinfo.css";
|
||||
@@ -0,0 +1,164 @@
|
||||
.easyuse-model-info {
|
||||
color: white;
|
||||
max-width: 90vw;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
.easyuse-model-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.easyuse-model-header{
|
||||
margin:0 0 15px 0;
|
||||
}
|
||||
.easyuse-model-header-remark{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top:5px;
|
||||
}
|
||||
.easyuse-model-info h2 {
|
||||
text-align: left;
|
||||
margin:0;
|
||||
}
|
||||
.easyuse-model-info h5 {
|
||||
text-align: left;
|
||||
margin:0 15px 0 0px;
|
||||
font-weight: 400;
|
||||
color:var(--descrip-text);
|
||||
}
|
||||
.easyuse-model-info p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
.easyuse-model-info a {
|
||||
color: var(--theme-color-light);
|
||||
}
|
||||
.easyuse-model-info a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.easyuse-model-tags-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
list-style: none;
|
||||
gap: 10px;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
margin: 10px 0;
|
||||
padding: 0;
|
||||
}
|
||||
.easyuse-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;
|
||||
}
|
||||
.easyuse-model-tag--selected span::before {
|
||||
content: "✅";
|
||||
position: absolute;
|
||||
background-color: var(--theme-color-light);
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.easyuse-model-tag:hover {
|
||||
outline: 2px solid var(--theme-color-light);
|
||||
}
|
||||
.easyuse-model-tag p {
|
||||
margin: 0;
|
||||
}
|
||||
.easyuse-model-tag span {
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
background-color: var(--theme-color-light);
|
||||
padding: 2px;
|
||||
position: relative;
|
||||
min-width: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.easyuse-model-metadata .comfy-modal-content {
|
||||
max-width: 100%;
|
||||
}
|
||||
.easyuse-model-metadata label {
|
||||
margin-right: 1ch;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.easyuse-model-metadata span {
|
||||
color: var(--theme-color-light);
|
||||
}
|
||||
|
||||
.easyuse-preview {
|
||||
max-width:660px;
|
||||
margin-right: 15px;
|
||||
position: relative;
|
||||
}
|
||||
.easyuse-preview-group{
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius:.5rem;
|
||||
width: 660px;
|
||||
}
|
||||
.easyuse-preview-list{
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
transition: all .5s ease-in-out;
|
||||
}
|
||||
.easyuse-preview-list.no-transition{
|
||||
transition: none;
|
||||
}
|
||||
.easyuse-preview-slide{
|
||||
display: flex;
|
||||
flex-basis: 50%;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.easyuse-preview-slide img {
|
||||
border-radius: 14px;
|
||||
object-position: center center;
|
||||
max-width: 100%;
|
||||
max-height:700px;
|
||||
border-style: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.easyuse-preview button {
|
||||
position: absolute;
|
||||
z-index:10;
|
||||
top: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width:30px;
|
||||
height:30px;
|
||||
border-radius:15px;
|
||||
border:1px solid rgba(66, 63, 78, .3);
|
||||
background-color: rgba(66, 63, 78, .5);
|
||||
color:hsla(0, 0%, 100%, .8);
|
||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
|
||||
transition-timing-function: cubic-bezier(.4,0,.2,1);
|
||||
transition-duration: .15s;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.easyuse-preview button.left{
|
||||
left:10px;
|
||||
}
|
||||
.easyuse-preview button.right{
|
||||
right:10px;
|
||||
}
|
||||
.easyuse-model-notes {
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
padding: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.easyuse-model-notes:empty {
|
||||
display: none;
|
||||
}
|
||||
+11
-1
@@ -2,6 +2,13 @@ import {getLocale} from './utils.js'
|
||||
const locale = getLocale()
|
||||
|
||||
const zhCN = {
|
||||
// ExtraMenu
|
||||
"💎 View Checkpoint Info...": "💎 查看 Checkpoint 信息...",
|
||||
"💎 View Lora Info...": "💎 查看 Lora 信息...",
|
||||
"🔃 Reload Node": "🔃 刷新节点",
|
||||
// ModelInfo
|
||||
"Updated At:": "最近更新:",
|
||||
"Created At:": "首次发布:",
|
||||
// GroupMap
|
||||
"Groups Map (EasyUse)": "管理组 (EasyUse)",
|
||||
"Always": "启用中",
|
||||
@@ -13,8 +20,11 @@ const zhCN = {
|
||||
"Enable ALT+1~9 to paste nodes from nodes template (ComfyUI-Easy-Use)": "启用ALT1~9从节点模板粘贴到工作流 (ComfyUI-Easy-Use)",
|
||||
"Enable process bar in queue button (ComfyUI-Easy-Use)": "启用提示词队列进度显示条 (ComfyUI-Easy-Use)",
|
||||
"Enable ContextMenu Auto Nest Subdirectories (ComfyUI-Easy-Use)": "启用上下文菜单自动嵌套子目录 (ComfyUI-Easy-Use)"
|
||||
|
||||
|
||||
}
|
||||
|
||||
export const $t = (key) => {
|
||||
return locale === 'zh-CN' ? zhCN[key] : key
|
||||
const cn = zhCN[key]
|
||||
return locale === 'zh-CN' && cn ? cn : key
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
import { $el, ComfyDialog } from "/scripts/ui.js";
|
||||
import { api } from "/scripts/api.js";
|
||||
import {formatTime} from './utils.js';
|
||||
import {$t} from "./i18n.js";
|
||||
|
||||
|
||||
class MetadataDialog extends ComfyDialog {
|
||||
constructor() {
|
||||
super();
|
||||
this.element.classList.add("easyuse-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("easyuse-model-info");
|
||||
}
|
||||
|
||||
get customNotes() {
|
||||
return this.metadata["easyuse.notes"];
|
||||
}
|
||||
|
||||
set customNotes(v) {
|
||||
this.metadata["easyuse.notes"] = v;
|
||||
}
|
||||
|
||||
get hash() {
|
||||
return this.metadata["easyuse.sha256"];
|
||||
}
|
||||
|
||||
async show(type, value) {
|
||||
this.type = type;
|
||||
|
||||
const req = api.fetchApi("/easyuse/metadata/" + encodeURIComponent(`${type}/${value}`));
|
||||
this.info = $el("div", { style: { flex: "auto" } });
|
||||
// this.img = $el("img", { style: { display: "none" } });
|
||||
this.imgCurrent = 0
|
||||
this.imgList = $el("div.easyuse-preview-list",{
|
||||
style: { display: "none" }
|
||||
})
|
||||
this.imgWrapper = $el("div.easyuse-preview", [
|
||||
$el("div.easyuse-preview-group",[
|
||||
this.imgList
|
||||
]),
|
||||
]);
|
||||
this.main = $el("main", { style: { display: "flex" } }, [this.imgWrapper, this.info]);
|
||||
this.content = $el("div.easyuse-model-content", [
|
||||
$el("div.easyuse-model-header",[$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.easyuse-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) => {
|
||||
// 变更标题信息
|
||||
let header = this.element.querySelector('.easyuse-model-header')
|
||||
if(header){
|
||||
header.replaceChildren(
|
||||
$el("h2", { textContent: this.name }),
|
||||
$el("div.easyuse-model-header-remark",[
|
||||
$el("h5", { textContent: $t("Updated At:") + formatTime(new Date(info.updatedAt),'yyyy/MM/dd')}),
|
||||
$el("h5", { textContent: $t("Created At:") + formatTime(new Date(info.updatedAt),'yyyy/MM/dd')}),
|
||||
])
|
||||
)
|
||||
}
|
||||
// 替换内容
|
||||
content.replaceChildren(
|
||||
$el("a", {
|
||||
href: "https://civitai.com/models/" + info.modelId,
|
||||
textContent: "View " + info.model.name,
|
||||
target: "_blank",
|
||||
})
|
||||
);
|
||||
|
||||
if (info.images?.length) {
|
||||
this.imgCurrent = 0
|
||||
info.images.map(cate=>
|
||||
cate.url &&
|
||||
this.imgList.appendChild(
|
||||
$el('div.easyuse-preview-slide',[
|
||||
$el('img',{src:(cate.url)})
|
||||
])
|
||||
)
|
||||
)
|
||||
let _this = this
|
||||
this.imgDistance = (-660 * this.imgCurrent).toString()
|
||||
this.imgList.style.display = ''
|
||||
this.imgList.style.transform = 'translate3d(' + this.imgDistance +'px, 0px, 0px)'
|
||||
this.slides = this.imgList.querySelectorAll('.easyuse-preview-slide')
|
||||
// 添加按钮
|
||||
this.slideLeftButton = $el("button.left",{
|
||||
parent: this.imgWrapper,
|
||||
innerHTML:`<svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16" style="transform: rotate(90deg);"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>`,
|
||||
onclick: ()=>{
|
||||
_this.imgList.classList.remove("no-transition")
|
||||
if(_this.imgCurrent == 0){
|
||||
_this.imgCurrent = (info.images.length/2)-1
|
||||
this.slides[this.slides.length-1].style.transform = 'translate3d(' + (-660 * (this.imgCurrent+1)).toString()+'px, 0px, 0px)'
|
||||
this.slides[this.slides.length-2].style.transform = 'translate3d(' + (-660 * (this.imgCurrent+1)).toString()+'px, 0px, 0px)'
|
||||
_this.imgList.style.transform = 'translate3d(660px, 0px, 0px)'
|
||||
setTimeout(_=>{
|
||||
this.slides[this.slides.length-1].style.transform = 'translate3d(0px, 0px, 0px)'
|
||||
this.slides[this.slides.length-2].style.transform = 'translate3d(0px, 0px, 0px)'
|
||||
_this.imgDistance = (-660 * this.imgCurrent).toString()
|
||||
_this.imgList.style.transform = 'translate3d(' + _this.imgDistance +'px, 0px, 0px)'
|
||||
_this.imgList.classList.add("no-transition")
|
||||
},500)
|
||||
}
|
||||
else {
|
||||
_this.imgCurrent = _this.imgCurrent-1
|
||||
_this.imgDistance = (-660 * this.imgCurrent).toString()
|
||||
_this.imgList.style.transform = 'translate3d(' + _this.imgDistance +'px, 0px, 0px)'
|
||||
}
|
||||
}
|
||||
})
|
||||
this.slideRightButton = $el("button.right",{
|
||||
parent: this.imgWrapper,
|
||||
innerHTML:`<svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16" style="transform: rotate(-90deg);"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>`,
|
||||
onclick: ()=>{
|
||||
_this.imgList.classList.remove("no-transition")
|
||||
if( _this.imgCurrent >= (info.images.length/2)-1){
|
||||
_this.imgCurrent = 0
|
||||
const max = info.images.length/2
|
||||
this.slides[0].style.transform = 'translate3d(' + (660 * max).toString()+'px, 0px, 0px)'
|
||||
this.slides[1].style.transform = 'translate3d(' + (660 * max).toString()+'px, 0px, 0px)'
|
||||
_this.imgList.style.transform = 'translate3d(' + (-660 * max).toString()+'px, 0px, 0px)'
|
||||
setTimeout(_=>{
|
||||
this.slides[0].style.transform = 'translate3d(0px, 0px, 0px)'
|
||||
this.slides[1].style.transform = 'translate3d(0px, 0px, 0px)'
|
||||
_this.imgDistance = (-660 * this.imgCurrent).toString()
|
||||
_this.imgList.style.transform = 'translate3d(' + _this.imgDistance +'px, 0px, 0px)'
|
||||
_this.imgList.classList.add("no-transition")
|
||||
},500)
|
||||
}
|
||||
else {
|
||||
_this.imgCurrent = _this.imgCurrent+1
|
||||
_this.imgDistance = (-660 * this.imgCurrent).toString()
|
||||
_this.imgList.style.transform = 'translate3d(' + _this.imgDistance +'px, 0px, 0px)'
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export 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 MAX_TAGS = 500
|
||||
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.easyuse-model-tag",
|
||||
{
|
||||
dataset: {
|
||||
tag: t[0],
|
||||
},
|
||||
$: (el) => {
|
||||
el.onclick = () => {
|
||||
el.classList.toggle("easyuse-model-tag--selected");
|
||||
};
|
||||
},
|
||||
},
|
||||
[
|
||||
$el("p", {
|
||||
textContent: t[0],
|
||||
}),
|
||||
$el("span", {
|
||||
textContent: t[1],
|
||||
}),
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
addTags() {
|
||||
let tags = this.getTagFrequency();
|
||||
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.easyuse-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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
if (info) {
|
||||
$el(
|
||||
"p",
|
||||
{
|
||||
parent: this.content,
|
||||
textContent: "Trained Words: ",
|
||||
},
|
||||
[
|
||||
$el("pre", {
|
||||
textContent: info.trainedWords.join(", "),
|
||||
style: {
|
||||
whiteSpace: "pre-wrap",
|
||||
margin: "10px 0",
|
||||
background: "#222",
|
||||
padding: "5px",
|
||||
borderRadius: "5px",
|
||||
maxHeight: "250px",
|
||||
overflow: "auto",
|
||||
},
|
||||
}),
|
||||
]
|
||||
);
|
||||
$el("div", {
|
||||
parent: this.content,
|
||||
innerHTML: info.description,
|
||||
style: {
|
||||
maxHeight: "250px",
|
||||
overflow: "auto",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
createButtons() {
|
||||
const btns = super.createButtons();
|
||||
|
||||
function copyTags(e, tags) {
|
||||
const textarea = $el("textarea", {
|
||||
parent: document.body,
|
||||
style: {
|
||||
position: "fixed",
|
||||
},
|
||||
textContent: tags.map((el) => el.dataset.tag).join(", "),
|
||||
});
|
||||
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: "Copy Selected",
|
||||
onclick: (e) => {
|
||||
copyTags(e, [...this.tags.querySelectorAll(".easyuse-model-tag--selected")]);
|
||||
},
|
||||
}),
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Copy All",
|
||||
onclick: (e) => {
|
||||
copyTags(e, [...this.tags.querySelectorAll(".easyuse-model-tag")]);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return btns;
|
||||
}
|
||||
}
|
||||
+19
-1
@@ -45,4 +45,22 @@ export function spliceExtension(fileName){
|
||||
}
|
||||
export function getExtension(fileName){
|
||||
return fileName.substring(fileName.lastIndexOf('.') + 1)
|
||||
}
|
||||
}
|
||||
|
||||
export function formatTime(time, format) {
|
||||
time = typeof (time) === "number" ? time : (time instanceof Date ? time.getTime() : parseInt(time));
|
||||
if (isNaN(time)) return null;
|
||||
if (typeof (format) !== 'string' || !format) format = 'yyyy-MM-dd hh:mm:ss';
|
||||
let _time = new Date(time);
|
||||
time = _time.toString().split(/[\s\:]/g).slice(0, -2);
|
||||
time[1] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'][_time.getMonth()];
|
||||
let _mapping = {
|
||||
MM: 1,
|
||||
dd: 2,
|
||||
yyyy: 3,
|
||||
hh: 4,
|
||||
mm: 5,
|
||||
ss: 6
|
||||
};
|
||||
return format.replace(/([Mmdhs]|y{2})\1/g, (key) => time[_mapping[key]]);
|
||||
}
|
||||
|
||||
+2
-1
@@ -18,6 +18,7 @@ app.registerExtension({
|
||||
emptyImg.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=";
|
||||
|
||||
options.push(null,
|
||||
// Groups Map
|
||||
{
|
||||
content: '📜 '+ $t('Groups Map (EasyUse)'),
|
||||
callback: async() => {
|
||||
@@ -250,7 +251,7 @@ app.registerExtension({
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
return options;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import {app} from "/scripts/app.js";
|
||||
import {$t} from '../common/i18n.js'
|
||||
import {CheckpointInfoDialog, LoraInfoDialog} from "../common/model.js";
|
||||
|
||||
const loaders = ['easy fullLoader', 'easy a1111Loader', 'easy comfyLoader']
|
||||
const preSampling = ['easy preSampling', 'easy preSamplingAdvanced', 'easy preSamplingDynamicCFG', 'easy preSamplingNoiseIn', 'easy preSamplingLayerDiffusion', 'easy fullkSampler']
|
||||
@@ -264,6 +266,25 @@ const addMenu = (content, type, nodes_include, nodeType, has_submenu=true) => {
|
||||
has_submenu: has_submenu,
|
||||
callback: (value, options, e, menu, node) => showSwapMenu(value, options, e, menu, node, type, nodes_include)
|
||||
})
|
||||
// if(type == 'loaders'){
|
||||
// options.unshift({
|
||||
// content: $t("💎 View Lora Info..."),
|
||||
// callback: (value, options, e, menu, node) => {
|
||||
// const widget = node.widgets.find(cate=> cate.name == 'lora_name')
|
||||
// let name = widget.value;
|
||||
// if (!name || name == 'None') return
|
||||
// new LoraInfoDialog(name).show('loras', name);
|
||||
// }
|
||||
// })
|
||||
// options.unshift({
|
||||
// content: $t("💎 View Checkpoint Info..."),
|
||||
// callback: (value, options, e, menu, node) => {
|
||||
// let name = node.widgets[0].value;
|
||||
// if (!name || name == 'None') return
|
||||
// new CheckpointInfoDialog(name).show('checkpoints', name);
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
})
|
||||
}
|
||||
const showSwapMenu = (value, options, e, menu, node, type, nodes_include) => {
|
||||
@@ -482,7 +503,7 @@ app.registerExtension({
|
||||
// 刷新节点
|
||||
addMenuHandler(nodeType, function (_, options) {
|
||||
options.unshift({
|
||||
content: "🔃 Reload Node",
|
||||
content: $t("🔃 Reload Node"),
|
||||
callback: (value, options, e, menu, node) => {
|
||||
let graphcanvas = LGraphCanvas.active_canvas;
|
||||
if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) {
|
||||
|
||||
Reference in New Issue
Block a user