From 70ca7cc35f1cdacc1d65d1b263b7ced254d0bbc3 Mon Sep 17 00:00:00 2001 From: yolain Date: Fri, 5 Apr 2024 12:33:18 +0800 Subject: [PATCH] add:COMOSITION of preset in easy ipadapterApply --- README.en.md | 1 + README.md | 1 + py/api.py | 63 +++- py/config.py | 8 + py/easyNodes.py | 8 +- py/libs/utils.py | 24 +- web/css/index.css | 3 +- web/css/modelinfo.css | 164 ++++++++++ web/js/common/i18n.js | 12 +- web/js/common/model.js | 615 +++++++++++++++++++++++++++++++++++ web/js/common/utils.js | 20 +- web/js/easy/easy.js | 3 +- web/js/easy/easyExtraMenu.js | 23 +- 13 files changed, 934 insertions(+), 11 deletions(-) create mode 100644 web/css/modelinfo.css create mode 100644 web/js/common/model.js 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) {