diff --git a/README.en.md b/README.en.md
index 5a16466..ab958db 100644
--- a/README.en.md
+++ b/README.en.md
@@ -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`
diff --git a/README.md b/README.md
index cda9c33..2a83530 100644
--- a/README.md
+++ b/README.md
@@ -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`
diff --git a/py/api.py b/py/api.py
index e3ff12d..f128f80 100644
--- a/py/api.py
+++ b/py/api.py
@@ -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 = {}
\ No newline at end of file
diff --git a/py/config.py b/py/config.py
index cc12c1d..2a7f508 100644
--- a/py/config.py
+++ b/py/config.py
@@ -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"
+ }
}
}
diff --git a/py/easyNodes.py b/py/easyNodes.py
index 83dcf1d..35ccd6a 100644
--- a/py/easyNodes.py
+++ b/py/easyNodes.py
@@ -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)$'
diff --git a/py/libs/utils.py b/py/libs/utils.py
index 2a8a72d..40cd265 100644
--- a/py/libs/utils.py
+++ b/py/libs/utils.py
@@ -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']
\ No newline at end of file
+ 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
\ No newline at end of file
diff --git a/web/css/index.css b/web/css/index.css
index 2069ddf..06e59db 100644
--- a/web/css/index.css
+++ b/web/css/index.css
@@ -2,4 +2,5 @@
@import "dropdown.css";
@import "selector.css";
@import "groupmap.css";
-@import "contextmenu.css";
\ No newline at end of file
+@import "contextmenu.css";
+@import "modelinfo.css";
\ No newline at end of file
diff --git a/web/css/modelinfo.css b/web/css/modelinfo.css
new file mode 100644
index 0000000..baed5af
--- /dev/null
+++ b/web/css/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;
+}
diff --git a/web/js/common/i18n.js b/web/js/common/i18n.js
index ce26b0d..3770e46 100644
--- a/web/js/common/i18n.js
+++ b/web/js/common/i18n.js
@@ -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
}
\ No newline at end of file
diff --git a/web/js/common/model.js b/web/js/common/model.js
new file mode 100644
index 0000000..001ab6c
--- /dev/null
+++ b/web/js/common/model.js
@@ -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", "
");
+ 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:``,
+ 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:``,
+ 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;
+ }
+}
\ No newline at end of file
diff --git a/web/js/common/utils.js b/web/js/common/utils.js
index 56ddf0e..0537b8a 100644
--- a/web/js/common/utils.js
+++ b/web/js/common/utils.js
@@ -45,4 +45,22 @@ export function spliceExtension(fileName){
}
export function getExtension(fileName){
return fileName.substring(fileName.lastIndexOf('.') + 1)
-}
\ No newline at end of file
+}
+
+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]]);
+}
diff --git a/web/js/easy/easy.js b/web/js/easy/easy.js
index a4daa7a..f79482b 100644
--- a/web/js/easy/easy.js
+++ b/web/js/easy/easy.js
@@ -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;
};
diff --git a/web/js/easy/easyExtraMenu.js b/web/js/easy/easyExtraMenu.js
index c1d4a33..0683485 100644
--- a/web/js/easy/easyExtraMenu.js
+++ b/web/js/easy/easyExtraMenu.js
@@ -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) {