Buckets:
| import struct | |
| import queue | |
| import platform | |
| import time | |
| import re | |
| import json | |
| import bpy | |
| import addon_utils | |
| from pathlib import Path | |
| from functools import lru_cache | |
| from urllib.parse import urlparse | |
| from ast import literal_eval | |
| from .kclogger import logger | |
| from .translations import LANG_TEXT | |
| from .timer import Timer | |
| from .datas import IMG_SUFFIX, get_bl_version | |
| translation = {} | |
| meta_info = {} | |
| def get_bl_info() -> dict: | |
| module = get_bl_module() | |
| return addon_utils.module_bl_info(module) or meta_info.get("bl_info", {}) | |
| def get_name(): | |
| return meta_info.get("package", "") | |
| def get_bl_module(name=None): | |
| if not name: | |
| name = get_name() | |
| try: | |
| return addon_utils.addons_fake_modules[name] | |
| except Exception: | |
| for mod in addon_utils.modules(refresh=False): | |
| if mod.__name__ == name: | |
| return mod | |
| def popup_folder(path: Path): | |
| import os | |
| if platform.system() == "Windows": | |
| if path.is_file(): | |
| path = path.parent | |
| path = path.as_posix() | |
| os.startfile(path) | |
| else: | |
| os.system(f"open {path}") | |
| def get_ai_mat_tree(obj: bpy.types.Object): | |
| if not obj or obj.type != "MESH": | |
| return None | |
| if hasattr(obj, "ai_mat_tree"): | |
| return getattr(obj, "ai_mat_tree") | |
| tree_name = obj.get("AI_Mat_Gen", "") | |
| if not tree_name: | |
| return None | |
| sdn_time_code = obj.get("AI_Mat_Gen_Id", "-1") | |
| tree = bpy.data.node_groups.get(tree_name, None) | |
| if not tree or tree.get("sdn_time_code", "0") != sdn_time_code: | |
| for ng in bpy.data.node_groups: | |
| if ng.get("sdn_time_code", "0") == sdn_time_code: | |
| return ng | |
| return None | |
| return tree | |
| def set_ai_mat_tree(obj: bpy.types.Object, tree: bpy.types.NodeTree): | |
| obj["AI_Mat_Gen"] = tree.name | |
| tree.set_sdn_time_code() | |
| obj["AI_Mat_Gen_Id"] = tree.sdn_time_code | |
| def read_json(path: Path | str) -> dict: | |
| import json | |
| encodings = ["utf8", "gbk"] | |
| for encoding in encodings: | |
| try: | |
| return json.loads(Path(path).read_text(encoding=encoding)) | |
| except UnicodeDecodeError: | |
| continue | |
| except json.JSONDecodeError: | |
| continue | |
| return {} | |
| def rmtree(path: Path): | |
| if path.is_file(): | |
| path.unlink() | |
| elif path.is_dir(): | |
| for child in path.iterdir(): | |
| rmtree(child) | |
| try: | |
| path.rmdir() # nas 的共享盘可能会有残留 | |
| except BaseException: | |
| ... | |
| def get_addon_name(): | |
| return "AI Node" + get_bl_version() | |
| def _T(word): | |
| if not isinstance(word, str): | |
| return word | |
| import bpy | |
| from bpy.app.translations import pgettext | |
| locale = bpy.context.preferences.view.language | |
| culture = translation.setdefault(locale, {}) | |
| if t := culture.get(word): | |
| return t | |
| def f(word): | |
| culture[word] = pgettext(word) | |
| Timer.put((f, word)) | |
| return LANG_TEXT.get(locale, {}).get(word, word) | |
| logger.set_translate(_T) | |
| def _T2(word): | |
| import bpy | |
| from .translations.translation import REPLACE_DICT | |
| locale = bpy.context.preferences.view.language | |
| return REPLACE_DICT.get(locale, {}).get(word, word) | |
| def find_areas_of_type(screen: bpy.types.Screen, area_type) -> list[bpy.types.Area]: | |
| return [area for area in screen.areas if area.type == area_type] | |
| def find_area_by_type(screen: bpy.types.Screen, area_type, index) -> bpy.types.Area: | |
| areas = find_areas_of_type(screen, area_type) | |
| if areas: | |
| return areas[index] | |
| return None | |
| def find_region_by_type(area: bpy.types.Area, region_type) -> bpy.types.Region: | |
| for region in area.regions: | |
| if region.type == region_type: | |
| return region | |
| return None | |
| def update_screen(): | |
| try: | |
| import bpy | |
| for area in bpy.context.screen.areas: | |
| area.tag_redraw() | |
| bpy.context.workspace.status_text_set_internal(None) | |
| except BaseException: | |
| ... | |
| def update_node_editor(): | |
| try: | |
| import bpy | |
| for area in bpy.context.screen.areas: | |
| for space in area.spaces: | |
| if space.type != "NODE_EDITOR": | |
| continue | |
| space.node_tree = space.node_tree | |
| if area.type == "NODE_EDITOR": | |
| area.tag_redraw() | |
| except Exception: | |
| ... | |
| def clear_cache(d=None): | |
| from shutil import rmtree as shutil_rmtree | |
| if not d: | |
| clear_cache(Path(__file__).parent) | |
| else: | |
| for file in Path(d).iterdir(): | |
| if not file.is_dir(): | |
| continue | |
| clear_cache(file) | |
| if file.name == "__pycache__": | |
| shutil_rmtree(file) | |
| def rgb2hex(r, g, b, *args): | |
| hex_val = f"#{int(r*256):02x}{int(g*256):02x}{int(b*256):02x}" | |
| return hex_val | |
| def hex2rgb(hex_val): | |
| hex_val = hex_val.lstrip('#') | |
| if len(hex_val) == 3: | |
| return [int(h, 16) / 16 for h in hex_val] | |
| return [int(hex_val[i:i + 2], 16) / 256 for i in (0, 2, 4)] | |
| def is_ipv6(ip): | |
| import ipaddress | |
| try: | |
| ipaddress.IPv6Address(ip) | |
| return True | |
| except ValueError: | |
| return False | |
| def is_ipv4(ip): | |
| import ipaddress | |
| try: | |
| ipaddress.IPv4Address(ip) | |
| return True | |
| except ValueError: | |
| return False | |
| def is_domain(ip): | |
| import re | |
| return re.match(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.[A-Za-z0-9-]{1,63}(?<!-))*\.[A-Za-z]{2,}$", ip) | |
| return re.match(r"^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$", ip) | |
| class PrevMgr: | |
| __PREV__ = {} | |
| def new(): | |
| import bpy.utils.previews | |
| import random | |
| prev = bpy.utils.previews.new() | |
| while (i := random.randint(0, 999999999)) in PrevMgr.__PREV__: | |
| continue | |
| PrevMgr.__PREV__[i] = prev | |
| return prev | |
| def remove(prev): | |
| import bpy.utils.previews | |
| bpy.utils.previews.remove(prev) | |
| def clear(): | |
| for prev in PrevMgr.__PREV__.values(): | |
| prev.clear() | |
| prev.close() | |
| PrevMgr.__PREV__.clear() | |
| def __del__(): | |
| PrevMgr.clear() | |
| class MetaIn(type): | |
| def __contains__(cls, name): | |
| return cls.__contains__(cls, name) | |
| class Icon(metaclass=MetaIn): | |
| PREV_DICT = PrevMgr.new() | |
| NONE_IMAGE = "" | |
| IMG_STATUS = {} | |
| PIX_STATUS = {} | |
| PATH2BPY = {} | |
| ENABLE_HQ_PREVIEW = False | |
| INSTANCE = None | |
| def __init__(self) -> None: | |
| if Icon.NONE_IMAGE and Icon.NONE_IMAGE not in Icon: | |
| Icon.NONE_IMAGE = FSWatcher.to_str(Icon.NONE_IMAGE) | |
| self.reg_icon(Icon.NONE_IMAGE) | |
| def __new__(cls, *args, **kwargs): | |
| if cls.INSTANCE is None: | |
| cls.INSTANCE = object.__new__(cls, *args, **kwargs) | |
| return cls.INSTANCE | |
| def update_path2bpy(): | |
| import bpy | |
| Icon.PATH2BPY.clear() | |
| for i in bpy.data.images: | |
| Icon.PATH2BPY[FSWatcher.to_str(i.filepath)] = i | |
| def apply_alpha(img): | |
| if img.file_format != "PNG" or img.channels < 4: | |
| return | |
| # 预乘alpha 到rgb | |
| import numpy as np | |
| pixels = np.zeros(img.size[0] * img.size[1] * 4, dtype=np.float32) | |
| img.pixels.foreach_get(pixels) | |
| sized_pixels = pixels.reshape(-1, 4) | |
| sized_pixels[:, :3] *= sized_pixels[:, 3].reshape(-1, 1) | |
| img.pixels.foreach_set(pixels) | |
| def clear(): | |
| Icon.PREV_DICT.clear() | |
| Icon.IMG_STATUS.clear() | |
| Icon.PIX_STATUS.clear() | |
| Icon.PATH2BPY.clear() | |
| Icon.reg_icon(Icon.NONE_IMAGE) | |
| def set_hq_preview(): | |
| from .preference import get_pref | |
| Icon.ENABLE_HQ_PREVIEW = get_pref().enable_hq_preview | |
| def try_mark_image(path) -> bool: | |
| p = FSWatcher.to_path(path) | |
| path = FSWatcher.to_str(path) | |
| if not p.exists(): | |
| return False | |
| if Icon.IMG_STATUS.get(path, -1) == p.stat().st_mtime_ns: | |
| return False | |
| return True | |
| def can_mark_image(path) -> bool: | |
| p = FSWatcher.to_path(path) | |
| path = FSWatcher.to_str(path) | |
| if not Icon.try_mark_image(p): | |
| return False | |
| Icon.IMG_STATUS[path] = p.stat().st_mtime_ns | |
| return True | |
| def can_mark_pixel(prev, name) -> bool: | |
| name = FSWatcher.to_str(name) | |
| if Icon.PIX_STATUS.get(name) == hash(prev.pixels): | |
| return False | |
| Icon.PIX_STATUS[name] = hash(prev.pixels) | |
| return True | |
| def remove_mark(name) -> bool: | |
| name = FSWatcher.to_str(name) | |
| Icon.IMG_STATUS.pop(name, None) | |
| Icon.PIX_STATUS.pop(name, None) | |
| Icon.PREV_DICT.pop(name, None) | |
| return True | |
| def reg_none(none: Path): | |
| none = FSWatcher.to_str(none) | |
| if none in Icon: | |
| return | |
| Icon.NONE_IMAGE = none | |
| Icon.reg_icon(Icon.NONE_IMAGE) | |
| def reg_icon(path, reload=False, hq=False): | |
| path = FSWatcher.to_str(path) | |
| if not Icon.can_mark_image(path): | |
| return Icon[path] | |
| if Icon.ENABLE_HQ_PREVIEW and hq: | |
| try: | |
| Icon.reg_icon_hq(path) | |
| except BaseException: | |
| Timer.put((Icon.reg_icon_hq, path)) | |
| return Icon[path] | |
| else: | |
| if path not in Icon: | |
| Icon.PREV_DICT.load(path, path, 'IMAGE') | |
| if reload: | |
| Timer.put(Icon.PREV_DICT[path].reload) | |
| return Icon[path] | |
| def reg_icon_hq(path): | |
| import bpy | |
| p = FSWatcher.to_path(path) | |
| path = FSWatcher.to_str(path) | |
| if path in Icon: | |
| return | |
| if p.exists() and p.suffix.lower() in IMG_SUFFIX: | |
| img = bpy.data.images.load(path) | |
| Icon.apply_alpha(img) | |
| Icon.reg_icon_by_pixel(img, path) | |
| Timer.put((bpy.data.images.remove, img)) # 直接使用 bpy.data.images.remove 会导致卡死 | |
| def find_image(path): | |
| img = Icon.PATH2BPY.get(FSWatcher.to_str(path), None) | |
| if not img: | |
| return None | |
| try: | |
| _ = img.name # hack ref detect | |
| return img | |
| except ReferenceError: | |
| Icon.update_path2bpy() | |
| return None | |
| def load_icon(path): | |
| import bpy | |
| p = FSWatcher.to_path(path) | |
| path = FSWatcher.to_str(path) | |
| # ctrl + z 导致bpy.data中的图像被删除 | |
| if path not in Icon.PATH2BPY: | |
| Icon.IMG_STATUS.pop(path, None) | |
| if not Icon.can_mark_image(path): | |
| return | |
| # if p.name[:63] in bpy.data.images: | |
| # img = bpy.data.images[p.name[:63]] | |
| # Icon.update_icon_pixel(img.name, img) | |
| if img := Icon.find_image(path): | |
| Icon.update_icon_pixel(path, img) | |
| return img | |
| elif p.suffix.lower() in IMG_SUFFIX: | |
| img = bpy.data.images.load(path) | |
| img.filepath = path | |
| Icon.apply_alpha(img) | |
| Icon.update_path2bpy() | |
| # img.name = path | |
| return img | |
| def reg_icon_by_pixel(prev, name): | |
| name = FSWatcher.to_str(name) | |
| if not Icon.can_mark_pixel(prev, name): | |
| return | |
| if name in Icon: | |
| return | |
| p = Icon.PREV_DICT.new(name) | |
| p.icon_size = (32, 32) | |
| p.image_size = (prev.size[0], prev.size[1]) | |
| p.image_pixels_float[:] = prev.pixels[:] | |
| def get_icon_id(name: Path): | |
| p = Icon.PREV_DICT.get(FSWatcher.to_str(name), None) | |
| if not p: | |
| p = Icon.PREV_DICT.get(FSWatcher.to_str(Icon.NONE_IMAGE), None) | |
| return p.icon_id if p else 0 | |
| def update_icon_pixel(name, prev): | |
| """ | |
| 更新bpy.data.image 时一并更新(因为pixel 的hash 不变) | |
| """ | |
| prev.reload() | |
| p = Icon.PREV_DICT.get(name, None) | |
| if not p: | |
| # logger.error("No") | |
| return | |
| p.icon_size = (32, 32) | |
| p.image_size = (prev.size[0], prev.size[1]) | |
| p.image_pixels_float[:] = prev.pixels[:] | |
| def __getitem__(self, name): | |
| return Icon.get_icon_id(name) | |
| def __contains__(self, name): | |
| return FSWatcher.to_str(name) in Icon.PREV_DICT | |
| def __class_getitem__(cls, name): | |
| return cls.__getitem__(cls, name) | |
| class PngParse: | |
| def read_head(pngpath): | |
| with open(pngpath, 'rb') as f: | |
| png_header = f.read(25) | |
| file_sig, ihdr_sig, width, height, bit_depth, color_type, \ | |
| compression_method, filter_method, interlace_method = \ | |
| struct.unpack('>8s4sIIBBBBB', png_header) | |
| # 输出 PNG 文件头 | |
| _ = { | |
| "PNG file signature": file_sig, | |
| "IHDR_signature": ihdr_sig, | |
| "Image_size": [width, height], | |
| "Bit_depth": bit_depth, | |
| "Color_type": color_type, | |
| "Compression_method": compression_method, | |
| "Filter_method": filter_method, | |
| "Interlace_method": interlace_method | |
| } | |
| def read_text_chunk(pngpath) -> dict[str, str]: | |
| data = {} | |
| with open(pngpath, 'rb') as file: | |
| signature = file.read(8) | |
| if signature != b'\x89PNG\r\n\x1a\n': | |
| print('Error: Not a PNG file') | |
| return data | |
| # IDHR, PLTE, sRGB, tEXt | |
| while True: | |
| length_bytes = file.read(4) | |
| length = struct.unpack('>I', length_bytes)[0] # Read chunk length (4 bytes) | |
| chunk_type = file.read(4) # Read chunk type (4 bytes) | |
| chunk_data = file.read(length) # Read chunk data (length bytes) | |
| _ = file.read(4) # Read CRC (4 bytes) | |
| if chunk_type in {b'IHDR', b'PLTE'}: # header and Palette | |
| continue | |
| elif chunk_type == b'tEXt': | |
| keyword, text = chunk_data.decode().split('\0', 1) | |
| data[keyword] = text | |
| elif chunk_type == b'IEND': | |
| break | |
| return data | |
| class PkgInstaller: | |
| source = [ | |
| "https://mirrors.aliyun.com/pypi/simple/", | |
| "https://pypi.tuna.tsinghua.edu.cn/simple/", | |
| "https://pypi.mirrors.ustc.edu.cn/simple/", | |
| "https://pypi.python.org/simple/", | |
| "https://pypi.org/simple", | |
| ] | |
| fast_url = "" | |
| def select_pip_source(): | |
| if not PkgInstaller.fast_url: | |
| import requests | |
| t, PkgInstaller.fast_url = 999, PkgInstaller.source[0] | |
| for url in PkgInstaller.source: | |
| try: | |
| tping = requests.get(url, timeout=1).elapsed.total_seconds() | |
| except Exception as e: | |
| logger.warning(e) | |
| continue | |
| if tping < 0.1: | |
| PkgInstaller.fast_url = url | |
| break | |
| if tping < t: | |
| t, PkgInstaller.fast_url = tping, url | |
| return PkgInstaller.fast_url | |
| def is_installed(package): | |
| import importlib | |
| try: | |
| return importlib.import_module(package) | |
| except ModuleNotFoundError: | |
| return False | |
| def prepare_pip(): | |
| import ensurepip | |
| if PkgInstaller.is_installed("pip"): | |
| return True | |
| try: | |
| ensurepip.bootstrap() | |
| return True | |
| except BaseException: | |
| ... | |
| return False | |
| def try_install(*packages): | |
| if not PkgInstaller.prepare_pip(): | |
| return False | |
| need = [pkg for pkg in packages if not PkgInstaller.is_installed(pkg)] | |
| from pip._internal import main | |
| if need: | |
| url = PkgInstaller.select_pip_source() | |
| for pkg in need: | |
| try: | |
| site = urlparse(url) | |
| # 避免build | |
| command = ['install', pkg, "-i", url, "--prefer-binary"] | |
| command.append("--trusted-host") | |
| command.append(site.netloc) | |
| main(command) | |
| if not PkgInstaller.is_installed(pkg): | |
| return False | |
| except Exception: | |
| return False | |
| return True | |
| class FSWatcher: | |
| """ | |
| 监听文件/文件夹变化的工具类 | |
| register: 注册监听, 传入路径和回调函数(可空) | |
| unregister: 注销监听 | |
| run: 监听循环, 使用单例,只在第一次初始化时调用 | |
| stop: 停止监听, 释放资源 | |
| consume_change: 消费变化, 当监听对象发生变化时记录为changed, 主动消费后置False, 用于自定义回调函数 | |
| """ | |
| _watcher_path: dict[Path, bool] = {} | |
| _watcher_stat = {} | |
| _watcher_callback = {} | |
| _watcher_queue = queue.Queue() | |
| _running = False | |
| _use_threading = False | |
| def init(cls) -> None: | |
| cls._run() | |
| def register(cls, path, callback=None): | |
| path = cls.to_path(path) | |
| if path in cls._watcher_path: | |
| return | |
| cls._watcher_path[path] = False | |
| cls._watcher_callback[path] = callback | |
| def unregister(cls, path): | |
| path = cls.to_path(path) | |
| cls._watcher_path.pop(path, None) | |
| cls._watcher_callback.pop(path, None) | |
| def _run(cls): | |
| if cls._running: | |
| return | |
| cls._running = True | |
| if cls._use_threading: | |
| # use threading | |
| from threading import Thread | |
| Thread(target=cls._loop, daemon=True).start() | |
| Thread(target=cls._run_ex, daemon=True).start() | |
| else: | |
| # use timer | |
| import bpy | |
| bpy.app.timers.register(cls._loop_timer, persistent=True) | |
| bpy.app.timers.register(cls._run_ex_timer, persistent=True) | |
| def _run_ex_timer(cls): | |
| cls._run_ex_one() | |
| return 0.5 | |
| def _run_ex(cls): | |
| while cls._running: | |
| cls._run_ex_one() | |
| time.sleep(0.1) | |
| def _run_ex_one(cls): | |
| if not cls._running: | |
| return | |
| while not cls._watcher_queue.empty(): | |
| path = cls._watcher_queue.get() | |
| if path not in cls._watcher_path: | |
| continue | |
| if callback := cls._watcher_callback[path]: | |
| callback(path) | |
| def _loop_timer(cls): | |
| cls._loop_one() | |
| return 1 | |
| def _loop(cls): | |
| """ | |
| 监听所有注册的路径, 有变化时记录为changed | |
| """ | |
| while cls._running: | |
| cls._loop_one() | |
| time.sleep(0.5) | |
| def _loop_one(cls): | |
| if not cls._running: | |
| return | |
| for path, changed in list(cls._watcher_path.items()): | |
| if changed: | |
| continue | |
| if not path.exists(): | |
| continue | |
| mtime = path.stat().st_mtime_ns | |
| if cls._watcher_stat.get(path, None) == mtime: | |
| continue | |
| cls._watcher_stat[path] = mtime | |
| cls._watcher_path[path] = True | |
| cls._watcher_queue.put(path) | |
| def stop(cls): | |
| cls._watcher_queue.put(None) | |
| cls._running = False | |
| def consume_change(cls, path) -> bool: | |
| path = cls.to_path(path) | |
| if path in cls._watcher_path and cls._watcher_path[path]: | |
| cls._watcher_path[path] = False | |
| return True | |
| return False | |
| def get_nas_mapping(cls): | |
| if platform.system() != "Windows": | |
| return {} | |
| import subprocess | |
| try: | |
| result = subprocess.run("net use", capture_output=True, text=True, encoding="gbk", check=True) | |
| except subprocess.CalledProcessError as e: | |
| logger.warning(e) | |
| return {} | |
| if result.returncode != 0 or result.stdout is None: | |
| return {} | |
| nas_mapping = {} | |
| try: | |
| lines = result.stdout.strip().split("\n")[4:] | |
| for line in lines: | |
| columns = line.split() | |
| if len(columns) < 3: | |
| continue | |
| local_drive = columns[1] + "/" | |
| nas_path = Path(columns[2]).resolve().as_posix() | |
| nas_mapping[local_drive] = nas_path | |
| except Exception: | |
| ... | |
| return nas_mapping | |
| def to_str(cls, path: Path): | |
| p = Path(path) | |
| try: | |
| res_str = p.resolve().as_posix() | |
| except FileNotFoundError as e: | |
| res_str = p.as_posix() | |
| logger.warning(e) | |
| # 处理nas路径 | |
| for local_drive, nas_path in cls.get_nas_mapping().items(): | |
| if not res_str.startswith(nas_path): | |
| continue | |
| return res_str.replace(nas_path, local_drive) | |
| return res_str | |
| def to_path(cls, path: Path): | |
| return Path(path) | |
| class ScopeTimer: | |
| def __init__(self, name: str = "", prt=print): | |
| self.name = name | |
| self.time_start = time.time() | |
| self.echo = prt | |
| def __del__(self): | |
| self.echo(f"{self.name} cost {time.time() - self.time_start:.4f}s") | |
| class CtxTimer: | |
| def __init__(self, name: str = "", prt=print): | |
| self.name = name | |
| self.time_start = time.time() | |
| self.echo = prt | |
| def __enter__(self): | |
| return self | |
| def __exit__(self, exc_type, exc_val, exc_tb): | |
| self.echo(f"{self.name} cost {time.time() - self.time_start:.4f}s") | |
| class WebUIToComfyUI: | |
| SAMPLERNAME_W2C = { | |
| "Euler": "euler", | |
| "Euler a": "euler_ancestral", | |
| "Heun": "heun", | |
| "DPM fast": "dpm_fast", | |
| "DPM adaptive": "dpm_adaptive", | |
| "DPM2": "dpm_2", | |
| "DPM2 a": "dpm_2_ancestral", | |
| "DPM++ 2M": "dpmpp_2m", | |
| "DPM++ SDE": "dpmpp_sde_gpu", | |
| "DPM++ 2M SDE": "dpmpp_2m_sde_gpu", | |
| "DPM++ 3M SDE": "dpmpp_3m_sde", | |
| "DDIM": "ddim", | |
| "LMS": "lms", | |
| "LCM": "LCM", | |
| "UniPC": "uni_pc", | |
| } | |
| SCHEDULERNAME_W2C = { | |
| "Automatic": "normal", | |
| "Karras": "karras", | |
| "Exponential": "exponential", | |
| "SGM Uniform": "sgm_uniform", | |
| } | |
| PREPROCESSOR_W2C = { | |
| "animal_openpose": "AnimalPosePreprocessor", | |
| "blur_gaussian": "*TilePreprocessor", | |
| "canny": "CannyEdgePreprocessor", | |
| "densepose (pruple bg & purple torso)": "DensePosePreprocessor", | |
| "densepose_parula (black bg & blue torso)": "DensePosePreprocessor", | |
| "depth_anything": "DepthAnythingPreprocessor", | |
| "depth_anything_v2": "DepthAnythingV2Preprocessor", | |
| "depth_hand_refiner": "MeshGraphormer+ImpactDetector-DepthMapPreprocessor", | |
| "depth_leres": "LeReS-DepthMapPreprocessor", | |
| "depth_leres++": "*LeReS-DepthMapPreprocessor", | |
| "depth_midas": "MiDaS-DepthMapPreprocessor", | |
| "depth_zoe": "Zoe-DepthMapPreprocessor", | |
| "dw_openpose_full": "DWPreprocessor", | |
| "facexlib": "", | |
| "inpaint_global_harmonious": "", | |
| "inpaint_only": "InpaintPreprocessor", | |
| "inpaint_only+lama": "", | |
| "instant_id_face_embedding": "", | |
| "instant_id_face_keypoints": "", | |
| "invert (from white bg & black line)": "ImageInvert", | |
| "ip-adapter-auto": "IPAdapter+IPAdapterUnifiedLoader", | |
| "ip-adapter_clip_g": "IPAdapter+IPAdapterUnifiedLoader", | |
| "ip-adapter_clip_h": "IPAdapter+IPAdapterUnifiedLoader", | |
| "ip-adapter_clip_sdxl_plus_vith": "IPAdapter+IPAdapterUnifiedLoader", | |
| "ip-adapter_face_id": "IPAdapterFaceID+IPAdapterUnifiedLoaderFaceID", | |
| "ip-adapter_face_id_plus": "IPAdapterFaceID+IPAdapterUnifiedLoaderFaceID", | |
| "ip-adapter_pulid": "", | |
| "lineart_anime": "AnimeLineArtPreprocessor", | |
| "lineart_anime_denoise": "", | |
| "lineart_coarse": "LineArtPreprocessor", | |
| "lineart_realistic": "LineArtPreprocessor", | |
| "lineart_standard (from white bg & black line)": "LineartStandardPreprocessor", | |
| "mediapipe_face": "MediaPipe-FaceMeshPreprocessor", | |
| "mlsd": "M-LSDPreprocessor", | |
| "none": "", | |
| "normal_bae": "BAE-NormalMapPreprocessor", | |
| "normal_dsine": "DSINE-NormalMapPreprocessor", | |
| "normal_midas": "MiDaS-NormalMapPreprocessor", | |
| "openpose": "OpenposePreprocessor", | |
| "openpose_face": "OpenposePreprocessor", | |
| "openpose_faceonly": "OpenposePreprocessor", | |
| "openpose_full": "OpenposePreprocessor", | |
| "openpose_hand": "OpenposePreprocessor", | |
| "recolor_intensity": "ImageIntensityDetector", | |
| "recolor_luminance": "ImageLuminanceDetector", | |
| "reference_adain": "", | |
| "reference_adain+attn": "", | |
| "reference_only": "", | |
| "revision_clipvision": "", | |
| "revision_ignore_prompt": "", | |
| "scribble_hed": "FakeScribblePreprocessor", | |
| "scribble_pidinet": "Scribble_PiDiNet_Preprocessor", | |
| "scribble_xdog": "Scribble_XDoG_Preprocessor", | |
| "seg_anime_face": "AnimeFace_SemSegPreprocessor", | |
| "seg_ofade20k": "OneFormer-ADE20K-SemSegPreprocessor", | |
| "seg_ofcoco": "OneFormer-COCO-SemSegPreprocessor", | |
| "seg_ufade20k": "UniFormer-SemSegPreprocessor", | |
| "shuffle": "ShufflePreprocessor", | |
| "softedge_anyline": "", | |
| "softedge_hed": "HEDPreprocessor", | |
| "softedge_hedsafe": "HEDPreprocessor", | |
| "softedge_pidinet": "PiDiNetPreprocessor", | |
| "softedge_pidisafe": "PiDiNetPreprocessor", | |
| "softedge_teed": "TEED_Preprocessor", | |
| "t2ia_color_grid": "ColorPreprocessor", | |
| "t2ia_sketch_pidi": "", | |
| "t2ia_style_clipvision": "", | |
| "threshold": "BinaryPreprocessor", | |
| "tile_colorfix": "", | |
| "tile_colorfix+sharp": "", | |
| "tile_resample": "TilePreprocessor", | |
| } | |
| def __init__(self, text: str = "", ): | |
| self.text: str = text | |
| self.params: dict = {} | |
| self.parse_cn = False | |
| def is_webui_format(self): | |
| return "Negative prompt: " in self.text and "Steps: " in self.text | |
| def get_registered_node_types(self): | |
| from .SDNode.nodes import NodeBase | |
| registered_node_types = {n.class_type: n.__metadata__ for n in NodeBase.__subclasses__()} | |
| return registered_node_types | |
| def with_efficient(self): | |
| registered_node_types = self.get_registered_node_types() | |
| return "Efficient Loader" in registered_node_types and "KSampler (Efficient)" in registered_node_types | |
| def apply_nodes_offset(this, nodes, offset=None): | |
| if offset is None: | |
| return | |
| for node in nodes: | |
| node["pos"][0] += offset[0] | |
| node["pos"][1] += offset[1] | |
| def find_following_nodes(self, wk, node, _nodes=None): | |
| if _nodes is None: | |
| _nodes = [] | |
| for out in node.get("outputs", []): | |
| for link_id in out.get("links", []) or []: | |
| _l = next((l for l in wk["links"] if l[0] == link_id), None) | |
| if not _l: | |
| continue | |
| _in = next((n for n in wk["nodes"] if n["id"] == _l[3]), None) | |
| if not _in: | |
| continue | |
| if _in not in _nodes: | |
| _nodes.append(_in) | |
| self.find_following_nodes(wk, _in, _nodes) | |
| return _nodes | |
| def make_link(self, workflow, out_node, out_index, in_node, in_index): | |
| last_link_id = workflow["last_link_id"] + 1 | |
| workflow["last_link_id"] = last_link_id | |
| ltype = out_node["outputs"][out_index]["type"] or None | |
| link = [last_link_id, out_node["id"], out_index, in_node["id"], in_index, ltype] | |
| out_node["outputs"][out_index]["links"].append(last_link_id) | |
| old_in_link = in_node["inputs"][in_index]["link"] | |
| if old_in_link and old_in_link != last_link_id: | |
| self.remove_link(workflow, old_in_link) | |
| in_node["inputs"][in_index]["link"] = last_link_id | |
| workflow["links"].append(link) | |
| def remove_link(self, workflow, link_id): | |
| if link_id is None: | |
| return | |
| if link_id == workflow["last_link_id"]: | |
| workflow["last_link_id"] = workflow["last_link_id"] - 1 | |
| for i in range(len(workflow["links"])): | |
| link = workflow["links"][i] | |
| if link[0] != link_id: | |
| continue | |
| workflow["links"].pop(i) | |
| return | |
| def remove_node_by_id(self, workflow, node_id): | |
| if not node_id: | |
| return | |
| find_node = None | |
| find_node_index = -1 | |
| for i in range(len(workflow["nodes"])): | |
| if (workflow["nodes"][i]["id"] == node_id): | |
| find_node = workflow["nodes"][i] | |
| find_node_index = i | |
| break | |
| if not find_node: | |
| return | |
| # 移除关联的link | |
| for inp in find_node.get("inputs", []): | |
| link_id = inp["link"] | |
| if not link_id: | |
| continue | |
| for node in workflow["nodes"]: | |
| for output in node["outputs"]: | |
| try: | |
| output["links"].remove(link_id) | |
| break | |
| except ValueError: | |
| ... | |
| self.remove_link(workflow, link_id) | |
| for out in find_node.get("outputs", []): | |
| for link_id in out.get("links", []): | |
| if link_id is None: | |
| continue | |
| for node in workflow["nodes"]: | |
| for inp in node.get("inputs", []): | |
| inp["link"] = None if inp["link"] == link_id else inp["link"] | |
| self.remove_link(workflow, link_id) | |
| # 移除节点 | |
| workflow["nodes"].pop(find_node_index) | |
| def to_comfyui_format(self): | |
| if self.with_efficient(): | |
| return self.to_comfyui_format_efficient() | |
| return self.to_comfyui_format_base() | |
| def to_comfyui_format_base(self): | |
| params = self.params.copy() | |
| wk = self.base_workflow() | |
| self.apply_nodes_offset(wk["nodes"], (-200, 63)) | |
| np = wk["nodes"][0] | |
| pp = wk["nodes"][1] | |
| empty_image = wk["nodes"][2] | |
| ksampler = wk["nodes"][3] | |
| checkpoint_loader = wk["nodes"][6] | |
| clip_last_layer = wk["nodes"][7] | |
| if "Negative prompt" in params: | |
| np["widgets_values"][0] = params["Negative prompt"] | |
| if "Positive prompt" in params: | |
| pp["widgets_values"][0] = params["Positive prompt"] | |
| if "Size" in params: | |
| width = 512 | |
| height = 512 | |
| if "x" in params["Size"]: | |
| size_list = params["Size"].split("x") | |
| width = size_list[0] | |
| height = size_list[1] | |
| empty_image["widgets_values"][0] = width | |
| empty_image["widgets_values"][1] = height | |
| if "Seed" in params: | |
| ksampler["widgets_values"][0] = params["Seed"] | |
| if "Steps" in params: | |
| ksampler["widgets_values"][2] = params["Steps"] | |
| if "CFG scale" in params: | |
| ksampler["widgets_values"][3] = params["CFG scale"] | |
| if "Sampler" in params: | |
| sampler_name: str = params["Sampler"] | |
| scheduler_name: str = "normal" | |
| if "Schedule type" in params: | |
| sampler_name = params["Sampler"] | |
| scheduler_name = params["Schedule type"] | |
| else: | |
| # samper存储 sampler_name + " " + scheduler_name | |
| for one_sch_name in self.SCHEDULERNAME_W2C: | |
| if one_sch_name in sampler_name: | |
| scheduler_name = one_sch_name | |
| sampler_name = sampler_name.replace(one_sch_name, "").strip() | |
| break | |
| if sampler_name in self.SAMPLERNAME_W2C: | |
| ksampler["widgets_values"][4] = self.SAMPLERNAME_W2C[sampler_name] | |
| if scheduler_name in self.SCHEDULERNAME_W2C: | |
| ksampler["widgets_values"][5] = self.SCHEDULERNAME_W2C[scheduler_name] | |
| self._gen_control_net(wk, ksampler, pp, np) | |
| if "Denoising strength" in params: | |
| ksampler["widgets_values"][6] = params["Denoising strength"] | |
| if float(params["Denoising strength"]) < 1: | |
| # 图生图, 需要添加图片输入 | |
| last_node_id = wk["last_node_id"] | |
| load_image = { | |
| "id": last_node_id + 1, | |
| "type": "LoadImage", | |
| "pos": [250, -110], | |
| "size": [320, 310], | |
| "mode": 0, | |
| "outputs": [ | |
| { | |
| "name": "IMAGE", | |
| "type": "IMAGE", | |
| "links": [], | |
| "shape": 3, | |
| "label": "图像", | |
| "slot_index": 0, | |
| }, | |
| { | |
| "name": "MASK", | |
| "type": "MASK", | |
| "links": None, | |
| "shape": 3, | |
| "label": "遮罩", | |
| }, | |
| ], | |
| "properties": {"Node name for S&R": "LoadImage"}, | |
| "widgets_values": ["xxx.png", "image"], | |
| } | |
| vae_encode = { | |
| "id": last_node_id + 2, | |
| "type": "VAEEncode", | |
| "pos": [640, 10], | |
| "size": {0: 210, 1: 50}, | |
| "mode": 0, | |
| "inputs": [ | |
| { | |
| "name": "pixels", | |
| "type": "IMAGE", | |
| "link": 0, | |
| "label": "图像", | |
| }, | |
| { | |
| "name": "vae", | |
| "type": "VAE", | |
| "link": None, | |
| "label": "VAE", | |
| }, | |
| ], | |
| "outputs": [ | |
| { | |
| "name": "LATENT", | |
| "type": "LATENT", | |
| "links": [], | |
| "shape": 3, | |
| "label": "Latent", | |
| "slot_index": 0, | |
| }, | |
| ], | |
| "properties": {"Node name for S&R": "VAEEncode"}, | |
| } | |
| wk["nodes"].append(load_image) | |
| wk["nodes"].append(vae_encode) | |
| wk["last_node_id"] = last_node_id + 2 | |
| self.remove_node_by_id(wk, empty_image["id"]) | |
| self.make_link(wk, load_image, 0, vae_encode, 0) | |
| self.make_link(wk, checkpoint_loader, 2, vae_encode, 1) | |
| self.make_link(wk, vae_encode, 0, ksampler, 3) | |
| if "Model" in params: | |
| model = params["Model"] # TODO: 模型得加后缀名字, 和webui不同 | |
| registered_node_types = self.get_registered_node_types() | |
| node_type = registered_node_types[checkpoint_loader["type"]] | |
| model_list = node_type.get("input", {}).get("required", {}).get("ckpt_name", [[]])[0] | |
| for _m in model_list: | |
| sep_i = _m.rfind("/") | |
| if _m[sep_i + 1:].split(".")[0] != model: | |
| continue | |
| checkpoint_loader["widgets_values"][0] = _m | |
| if "Clip skip" in params: | |
| clip_last_layer["widgets_values"][0] = -int(params["Clip skip"]) | |
| return json.dumps(wk) | |
| def to_comfyui_format_efficient(self): | |
| params = self.params.copy() | |
| wk = self.efficient_workflow() | |
| loader = wk["nodes"][1] | |
| ksampler = wk["nodes"][2] | |
| if "Negative prompt" in params: | |
| loader["widgets_values"][7] = params["Negative prompt"] | |
| if "Positive prompt" in params: | |
| loader["widgets_values"][6] = params["Positive prompt"] | |
| if "Size" in params: | |
| width = 512 | |
| height = 512 | |
| if "x" in params["Size"]: | |
| size_list = params["Size"].split("x") | |
| width = size_list[0] | |
| height = size_list[1] | |
| loader["widgets_values"][10] = width | |
| loader["widgets_values"][11] = height | |
| if "Seed" in params: | |
| ksampler["widgets_values"][0] = params["Seed"] | |
| if "Steps" in params: | |
| ksampler["widgets_values"][2] = params["Steps"] | |
| if "CFG scale" in params: | |
| ksampler["widgets_values"][3] = params["CFG scale"] | |
| if "Sampler" in params: | |
| sampler_name: str = params["Sampler"] | |
| scheduler_name: str = "normal" | |
| if "Schedule type" in params: | |
| sampler_name = params["Sampler"] | |
| scheduler_name = params["Schedule type"] | |
| else: | |
| # samper存储 sampler_name + " " + scheduler_name | |
| for one_sch_name in self.SCHEDULERNAME_W2C: | |
| if one_sch_name in sampler_name: | |
| scheduler_name = one_sch_name | |
| sampler_name = sampler_name.replace(one_sch_name, "").strip() | |
| break | |
| if sampler_name in self.SAMPLERNAME_W2C: | |
| ksampler["widgets_values"][4] = self.SAMPLERNAME_W2C[sampler_name] | |
| if scheduler_name in self.SCHEDULERNAME_W2C: | |
| ksampler["widgets_values"][5] = self.SCHEDULERNAME_W2C[scheduler_name] | |
| self._gen_control_net(wk, ksampler, loader, loader) | |
| if "Denoising strength" in params: | |
| ksampler["widgets_values"][6] = params["Denoising strength"] | |
| if float(params["Denoising strength"]) < 1: | |
| # 图生图, 需要添加图片输入 | |
| last_node_id = wk["last_node_id"] | |
| load_image = { | |
| "id": last_node_id + 1, | |
| "type": "输入图像", | |
| "pos": [210, -110], | |
| "size": {0: 200, 1: 100}, | |
| "mode": 0, | |
| "inputs": [], | |
| "outputs": [ | |
| { | |
| "name": "IMAGE", | |
| "type": "IMAGE", | |
| "links": [], | |
| "slot_index": 0 | |
| }, | |
| { | |
| "name": "MASK", | |
| "type": "MASK", | |
| "links": [], | |
| "slot_index": 1 | |
| } | |
| ], | |
| "title": "输入图像", | |
| "properties": {}, | |
| "widgets_values": ["", "输入"] | |
| } | |
| vae_encode = { | |
| "id": last_node_id + 2, | |
| "type": "VAEEncode", | |
| "pos": [640, 10], | |
| "size": {0: 210, 1: 50}, | |
| "mode": 0, | |
| "inputs": [ | |
| { | |
| "name": "pixels", | |
| "type": "IMAGE", | |
| "link": 0, | |
| "label": "图像", | |
| }, | |
| { | |
| "name": "vae", | |
| "type": "VAE", | |
| "link": None, | |
| "label": "VAE", | |
| }, | |
| ], | |
| "outputs": [ | |
| { | |
| "name": "LATENT", | |
| "type": "LATENT", | |
| "links": [], | |
| "shape": 3, | |
| "label": "Latent", | |
| "slot_index": 0, | |
| }, | |
| ], | |
| "properties": {"Node name for S&R": "VAEEncode"}, | |
| } | |
| wk["nodes"].append(load_image) | |
| wk["nodes"].append(vae_encode) | |
| wk["last_node_id"] = last_node_id + 2 | |
| self.make_link(wk, load_image, 0, vae_encode, 0) | |
| self.make_link(wk, loader, 4, vae_encode, 1) | |
| self.make_link(wk, vae_encode, 0, ksampler, 3) | |
| if "Model" in params: | |
| model = params["Model"] # 模型得加后缀名字, 和webui不同 | |
| registered_node_types = self.get_registered_node_types() | |
| node_type = registered_node_types[loader["type"]] | |
| model_list = node_type.get("input", {}).get("required", {}).get("ckpt_name", [[]])[0] | |
| for _m in model_list: | |
| sep_i = _m.rfind("/") | |
| if _m[sep_i + 1:].split(".")[0] != model: | |
| continue | |
| loader["widgets_values"][0] = _m | |
| if "Clip skip" in params: | |
| loader["widgets_values"][2] = -int(params["Clip skip"]) | |
| return json.dumps(wk) | |
| def parse(self, text=None): | |
| self.text = text or self.text | |
| # self.test() | |
| self.parse_cn = True | |
| self._parse(text) | |
| return self.params | |
| def _parse(self, text=None): | |
| if text: | |
| self.text = text.strip() | |
| self.params = {} | |
| self._prompt() | |
| self._base() | |
| self._control_net() | |
| self._ti_hashes() | |
| self._tiled_diffusion() | |
| self._adetailer() | |
| self._version() | |
| return self.params | |
| def _prompt(self): | |
| pp = re.search("^(.*?)Negative prompt:", self.text, re.S) | |
| if pp: | |
| pp = pp[1].strip() | |
| pp_str = pp[:-1].strip() if pp[-1] == "," else pp | |
| if pp_str.startswith("parameters"): | |
| pp_str = pp_str[len("parameters"):].strip() | |
| self.params["Positive prompt"] = pp_str | |
| self.text = self.text.replace(pp, "").strip() | |
| np = re.search("(Negative prompt: .*?)(?:Steps: )", self.text, re.S) | |
| np = np if np else re.search("Negative prompt: (.*?)(?:,\r\n)", self.text, re.S) | |
| np = np if np else re.search("Negative prompt: (.*?)(?:,\n)", self.text, re.S) | |
| np = np if np else re.search("Negative prompt: (.*?)(?:\n)", self.text, re.S) | |
| if np: | |
| prompt = np[1][len("Negative prompt: "):].strip() | |
| prompt = prompt[:-1].strip() if prompt[-1] == "," else prompt | |
| self.params["Negative prompt"] = prompt | |
| self.text = self.text.replace(np[1], "").strip() | |
| def _control_net(self): | |
| if not re.search(r"(Control[nN]et \d+): ", self.text, re.S): | |
| return | |
| def parse_cn_params(text): | |
| params = {} | |
| new_ver = "preprocessor params: " in text | |
| if new_ver: | |
| # Controlnet 0: "preprocessor: dw_openpose_full, model: | |
| # control_v11p_sd15_openpose, weight: 1.0, starting/ending: (0.0, 1.0), | |
| # resize mode: Crop and Resize, pixel_perfect: False, control mode: | |
| # Balanced, preprocessor params: (1024, None, None)", | |
| preprocessor = re.search("preprocessor: (.*?), ", text, re.S)[1] | |
| model = re.search("model: (.*?), ", text, re.S)[1] | |
| weight = re.search("weight: (.*?), ", text, re.S)[1] | |
| starting_end = re.search(r"starting\/ending: \((.*?)\),", text, re.S)[1] | |
| starting_end = literal_eval(f"[{starting_end}]") if starting_end else [] | |
| resize_mode = re.search("resize mode: (.*?), ", text, re.S)[1] | |
| pixel_perfect = re.search("pixel_perfect: (.*?), ", text, re.S)[1] == "True" | |
| control_mode = re.search("control mode: (.*?),", text, re.S)[1] | |
| pp_params = re.search(r"preprocessor params: \((.*?)\)", text, re.S)[1] | |
| # 去掉括号并按逗号分割 | |
| pp_params = literal_eval(f"[{pp_params}]") if pp_params else [] | |
| params = { | |
| "preprocessor": preprocessor, | |
| "model": model, | |
| "weight": weight, | |
| "starting_end": starting_end, | |
| "resize_mode": resize_mode, | |
| "preprocessor_params": pp_params, | |
| "pixel_perfect": pixel_perfect, | |
| "control_mode": control_mode, | |
| } | |
| else: | |
| # "ControlNet 0": "Module: tile_resample, Model: control_v11f1e_sd15_tile_fp16 [3b860298], Weight: 0.6, Resize Mode: Crop and Resize, Processor Res: 512, Threshold A: 1.0, Threshold B: 0.5, Guidance Start: 0.0, Guidance End: 1.0, Pixel Perfect: True, Control Mode: Balanced", | |
| preprocessor = re.search("Module: (.*?), ", text, re.S)[1] | |
| model = re.search("Model: (.*?), ", text, re.S)[1] | |
| weight = re.search("Weight: (.*?), ", text, re.S)[1] | |
| resize_mode = re.search("Resize Mode: (.*?), ", text, re.S)[1] | |
| pp_param_res = re.search("Processor Res: (.*?), ", text, re.S)[1] | |
| pp_param_a = re.search("Threshold A: (.*?), ", text, re.S)[1] | |
| pp_param_b = re.search("Threshold B: (.*?), ", text, re.S)[1] | |
| starting = re.search("Guidance Start: (.*?), ", text, re.S)[1] | |
| end = re.search("Guidance End: (.*?), ", text, re.S)[1] | |
| pixel_perfect = re.search("Pixel Perfect: (.*?), ", text, re.S)[1] | |
| pixel_perfect = pixel_perfect == "True" | |
| control_mode = re.search("Control Mode: (.*?)$", text, re.S)[1] | |
| params = { | |
| "preprocessor": preprocessor, | |
| "model": model, | |
| "weight": weight, | |
| "starting_end": [starting, end], | |
| "resize_mode": resize_mode, | |
| "preprocessor_params": [pp_param_res, pp_param_a, pp_param_b], | |
| "pixel_perfect": pixel_perfect, | |
| "control_mode": control_mode, | |
| } | |
| return params | |
| for cn in re.finditer(r'(Control[nN]et \d+): "(.*?)",', self.text, re.S): | |
| self.params[cn[1]] = parse_cn_params(cn[2]) if self.parse_cn else cn[2] | |
| self.text = self.text.replace(cn[0], "") | |
| def _gen_control_net(this, wk, ksampler, out_p, out_n): | |
| offset = 500 | |
| # CN部分 | |
| load_image_cn = { | |
| "id": wk["last_node_id"] + 1, | |
| "type": "LoadImage", | |
| "pos": [320, 800], | |
| "mode": 0, | |
| "outputs": [ | |
| { | |
| "name": "IMAGE", | |
| "type": "IMAGE", | |
| "links": [], | |
| "shape": 3, | |
| "label": "图像", | |
| "slot_index": 0, | |
| }, | |
| { | |
| "name": "MASK", | |
| "type": "MASK", | |
| "links": None, | |
| "shape": 3, | |
| "label": "遮罩", | |
| }, | |
| ], | |
| "widgets_values": ["xxx.png", "image"], | |
| } | |
| wk["nodes"].append(load_image_cn) | |
| wk["last_node_id"] += 1 | |
| count = -1 | |
| for key, value in this.params.items(): | |
| if not key.lower().startswith("controlnet"): | |
| continue | |
| count += 1 | |
| last_node_id = wk["last_node_id"] | |
| # 新增 aux 集成 preproccessor | |
| aux_preprocessor = { | |
| "id": last_node_id + 1, | |
| "type": "AIO_Preprocessor", | |
| "pos": [700 + offset * count, 640], | |
| "mode": 0, | |
| "inputs": [ | |
| { | |
| "name": "image", | |
| "type": "IMAGE", | |
| "link": None, | |
| "label": "图像", | |
| }, | |
| ], | |
| "outputs": [ | |
| { | |
| "name": "IMAGE", | |
| "type": "IMAGE", | |
| "links": [], | |
| "shape": 3, | |
| "label": "图像", | |
| "slot_index": 0, | |
| }, | |
| ], | |
| "widgets_values": ["CannyEdgePreprocessor", 512], | |
| } | |
| # 新增 apply controlnet | |
| apply_controlnet = { | |
| "id": last_node_id + 2, | |
| "type": "ControlNetApplyAdvanced", | |
| "pos": [710 + offset * count, 270], | |
| "mode": 0, | |
| "inputs": [ | |
| { | |
| "name": "positive", | |
| "type": "CONDITIONING", | |
| "link": None, | |
| "label": "正面条件", | |
| }, | |
| { | |
| "name": "negative", | |
| "type": "CONDITIONING", | |
| "link": None, | |
| "label": "负面条件", | |
| }, | |
| { | |
| "name": "control_net", | |
| "type": "CONTROL_NET", | |
| "link": None, | |
| "label": "ControlNet", | |
| }, | |
| { | |
| "name": "image", | |
| "type": "IMAGE", | |
| "link": None, | |
| "label": "图像", | |
| }, | |
| ], | |
| "outputs": [ | |
| { | |
| "name": "positive", | |
| "type": "CONDITIONING", | |
| "links": [], | |
| "shape": 3, | |
| "label": "正面条件", | |
| "slot_index": 0, | |
| }, | |
| { | |
| "name": "negative", | |
| "type": "CONDITIONING", | |
| "links": [20], | |
| "shape": 3, | |
| "label": "负面条件", | |
| "slot_index": 1, | |
| }, | |
| ], | |
| "widgets_values": [1, 0, 1], | |
| } | |
| controlnet_loader = { | |
| "id": last_node_id + 3, | |
| "type": "ControlNetLoader", | |
| "pos": [720 + offset * count, 500], | |
| "mode": 0, | |
| "outputs": [ | |
| { | |
| "name": "CONTROL_NET", | |
| "type": "CONTROL_NET", | |
| "links": [], | |
| "shape": 3, | |
| "label": "ControlNet", | |
| }, | |
| ], | |
| "widgets_values": ["xxx.pth", None], | |
| } | |
| wk["last_node_id"] += 3 | |
| wk["nodes"].extend([aux_preprocessor, apply_controlnet, controlnet_loader]) | |
| # 参数处理 | |
| if value["preprocessor"] in this.PREPROCESSOR_W2C: | |
| pmodel = this.PREPROCESSOR_W2C[value["preprocessor"]] | |
| pmodel = pmodel or aux_preprocessor["widgets_values"][0] | |
| aux_preprocessor["widgets_values"][0] = pmodel | |
| aux_preprocessor["widgets_values"][1] = value["preprocessor_params"][0] | |
| # controlnet_loader["widgets_values"][0] = value["model"] | |
| cn_model = value["model"] | |
| if "[" in cn_model and "]" in cn_model: | |
| cn_model = cn_model[: cn_model.find("[")].strip() | |
| cn_model2 = cn_model.replace("_", "-") | |
| registered_node_types = this.get_registered_node_types() | |
| node_type = registered_node_types[controlnet_loader["type"]] | |
| ml = node_type.get("input", {}).get("required", {}).get("control_net_name", [[]])[0] | |
| ml = ml or [] | |
| find_cn_model = ml[0] if ml else "" | |
| for _m in ml or []: | |
| sep_i = _m.rfind("/") | |
| if (_m[:sep_i + 1].split(".")[0] in [cn_model, cn_model2]): | |
| find_cn_model = _m | |
| controlnet_loader["widgets_values"][0] = find_cn_model | |
| apply_controlnet["widgets_values"][0] = value["weight"] | |
| apply_controlnet["widgets_values"][1] = value["starting_end"][0] | |
| apply_controlnet["widgets_values"][2] = value["starting_end"][1] | |
| this.make_link(wk, load_image_cn, 0, aux_preprocessor, 0) | |
| this.make_link(wk, controlnet_loader, 0, apply_controlnet, 2) | |
| this.make_link(wk, aux_preprocessor, 0, apply_controlnet, 3) | |
| # 完美像素 | |
| if value["pixel_perfect"]: | |
| aux_preprocessor["inputs"].append({ | |
| "name": "resolution", | |
| "type": "INT", | |
| "link": None, | |
| "widget": { | |
| "name": "resolution", | |
| }, | |
| "label": "分辨率", | |
| }) | |
| gen_res = { | |
| "id": wk["last_node_id"] + 1, | |
| "type": "ImageGenResolutionFromImage", | |
| "pos": [690 + offset * count, 950], | |
| "mode": 0, | |
| "inputs": [ | |
| { | |
| "name": "image", | |
| "type": "IMAGE", | |
| "link": None, | |
| "label": "图像", | |
| }, | |
| ], | |
| "outputs": [ | |
| { | |
| "name": "IMAGE_GEN_WIDTH (INT)", | |
| "type": "INT", | |
| "links": [], | |
| "shape": 3, | |
| "label": "宽度(整数)", | |
| "slot_index": 0, | |
| }, | |
| { | |
| "name": "IMAGE_GEN_HEIGHT (INT)", | |
| "type": "INT", | |
| "links": [], | |
| "shape": 3, | |
| "label": "高度(整数)", | |
| "slot_index": 1, | |
| }, | |
| ], | |
| } | |
| pixel_perfect = { | |
| "id": wk["last_node_id"] + 2, | |
| "type": "PixelPerfectResolution", | |
| "pos": [680 + offset * count, 780], | |
| "mode": 0, | |
| "inputs": [ | |
| { | |
| "name": "original_image", | |
| "type": "IMAGE", | |
| "link": None, | |
| "label": "图像", | |
| }, | |
| { | |
| "name": "image_gen_width", | |
| "type": "INT", | |
| "link": None, | |
| "widget": { | |
| "name": "image_gen_width", | |
| }, | |
| "label": "宽度", | |
| }, | |
| { | |
| "name": "image_gen_height", | |
| "type": "INT", | |
| "link": None, | |
| "widget": { | |
| "name": "image_gen_height", | |
| }, | |
| "label": "高度", | |
| }, | |
| ], | |
| "outputs": [ | |
| { | |
| "name": "RESOLUTION (INT)", | |
| "type": "INT", | |
| "links": [], | |
| "shape": 3, | |
| "label": "分辨率(整数)", | |
| "slot_index": 0, | |
| }, | |
| ], | |
| "widgets_values": [512, 512, "Just Resize"], | |
| } | |
| wk["last_node_id"] += 2 | |
| wk["nodes"].extend([gen_res, pixel_perfect]) | |
| this.make_link(wk, load_image_cn, 0, gen_res, 0) | |
| this.make_link(wk, load_image_cn, 0, pixel_perfect, 0) | |
| this.make_link(wk, gen_res, 0, pixel_perfect, 1) | |
| this.make_link(wk, gen_res, 1, pixel_perfect, 2) | |
| this.make_link(wk, pixel_perfect, 0, aux_preprocessor, 1) | |
| if out_p["type"] == "Efficient Loader": | |
| this.make_link(wk, out_p, 1, apply_controlnet, 0) | |
| this.make_link(wk, out_n, 2, apply_controlnet, 1) | |
| elif out_p["type"] == "CLIPTextEncode": | |
| this.make_link(wk, out_p, 0, apply_controlnet, 0) | |
| this.make_link(wk, out_n, 0, apply_controlnet, 1) | |
| elif out_p["type"] == "ControlNetApplyAdvanced": | |
| this.make_link(wk, out_p, 0, apply_controlnet, 0) | |
| this.make_link(wk, out_n, 1, apply_controlnet, 1) | |
| out_p = apply_controlnet | |
| out_n = apply_controlnet | |
| this.make_link(wk, apply_controlnet, 0, ksampler, 1) | |
| this.make_link(wk, apply_controlnet, 1, ksampler, 2) | |
| follow_nodes = [ksampler] | |
| this.find_following_nodes(wk, ksampler, follow_nodes) | |
| this.apply_nodes_offset(follow_nodes, (offset, 0)) | |
| def _ti_hashes(self): | |
| th = re.search('TI hashes: (".*?"),', self.text, re.S) | |
| if not th: | |
| return | |
| self.params["TI hashes"] = th[1].strip() | |
| self.text = self.text.replace(th[0], "").strip() | |
| def _tiled_diffusion(self): | |
| td = re.search("Tiled Diffusion: ({.*?}),", self.text, re.S) | |
| if not td: | |
| return | |
| self.params["Tiled Diffusion"] = td[1].strip() | |
| self.text = self.text.replace(td[0], "").strip() | |
| def _adetailer(self): | |
| ad_p = re.search('ADetailer prompt: (".*?"),', self.text, re.S) | |
| if ad_p: | |
| self.params["ADetailer prompt"] = ad_p[1].strip() | |
| self.text = self.text.replace(ad_p[0], "").strip() | |
| ads = re.finditer("(ADetailer .*?): (.*?),", self.text, re.S) | |
| for ad in ads: | |
| self.params[ad[1]] = ad[2] | |
| self.text = self.text.replace(ad[0], "") | |
| def _version(self): | |
| v = re.search(r"Version: (.*?)(?:.\s|$)", self.text, re.S) | |
| if not v: | |
| return | |
| self.params["Version"] = v[1].strip() | |
| self.text = self.text.replace(v[0], "").strip() | |
| def _base(self): | |
| step = re.search("Steps: (.*?),", self.text, re.S) | |
| if step: | |
| self.params["Steps"] = step[1].strip() | |
| self.text = self.text.replace(step[0], "").strip() | |
| sampler = re.search("Sampler: (.*?),", self.text, re.S) | |
| if sampler: | |
| self.params["Sampler"] = sampler[1].strip() | |
| self.text = self.text.replace(sampler[0], "").strip() | |
| scheduler = re.search("Schedule type: (.*?),", self.text, re.S) | |
| if scheduler: | |
| self.params["Schedule type"] = scheduler[1].strip() | |
| self.text = self.text.replace(scheduler[0], "").strip() | |
| cfg = re.search("CFG scale: (.*?),", self.text, re.S) | |
| if cfg: | |
| self.params["CFG scale"] = cfg[1].strip() | |
| self.text = self.text.replace(cfg[0], "").strip() | |
| seed = re.search("Seed: (.*?),", self.text, re.S) | |
| if seed: | |
| self.params["Seed"] = seed[1].strip() | |
| self.text = self.text.replace(seed[0], "").strip() | |
| size = re.search("Size: (.*?),", self.text, re.S) | |
| if size: | |
| self.params["Size"] = size[1].strip() | |
| self.text = self.text.replace(size[0], "").strip() | |
| model_hash = re.search("Model hash: (.*?),", self.text, re.S) | |
| if model_hash: | |
| self.params["Model hash"] = model_hash[1].strip() | |
| self.text = self.text.replace(model_hash[0], "").strip() | |
| model = re.search("Model: (.*?),", self.text, re.S) | |
| if model: | |
| self.params["Model"] = model[1].strip() | |
| self.text = self.text.replace(model[0], "").strip() | |
| denoising = re.search("Denoising strength: (.*?),", self.text, re.S) | |
| if denoising: | |
| self.params["Denoising strength"] = denoising[1].strip() | |
| self.text = self.text.replace(denoising[0], "").strip() | |
| clip_skip = re.search("Clip skip: (.*?),", self.text, re.S) | |
| if clip_skip: | |
| self.params["Clip skip"] = clip_skip[1].strip() | |
| self.text = self.text.replace(clip_skip[0], "").strip() | |
| vae = re.search("VAE: (.*?),", self.text, re.S) | |
| if vae: | |
| self.params["VAE"] = vae[1].strip() | |
| self.text = self.text.replace(vae[0], "").strip() | |
| vae_hash = re.search("VAE hash: (.*?),", self.text, re.S) | |
| if vae_hash: | |
| self.params["VAE hash"] = vae_hash[1].strip() | |
| self.text = self.text.replace(vae_hash[0], "").strip() | |
| def test(self): | |
| in_t0 = """ | |
| masterpiece,best quality,1girl, | |
| BREAK thighhighs, | |
| BREAK (colorful spot black:1.5),color gradient, | |
| BREAK multicolored background, | |
| Negative prompt: nsfw,nipples,navel,cameltoe,lowres,bad anatomy,bad hands,text,error,missing fingers,extra digit,fewer digits,cropped,worst quality,low quality,normal quality,jpeg artifacts,signature,watermark,username,blurry, | |
| Steps: 8, Sampler: DPM++ 2M Karras, CFG scale: 2, Seed: 3627297328, Size: 768x1024, Model hash: bbd321d4a3, Model: raemuXL_v35Lightning, Denoising strength: 0.5, Clip skip: 2, ADetailer model: face_yolov8n.pt, ADetailer prompt: "black eyes, black hair, ", ADetailer confidence: 0.3, ADetailer dilate erode: 4, ADetailer mask blur: 4, ADetailer denoising strength: 0.4, ADetailer inpaint only masked: True, ADetailer inpaint padding: 32, ADetailer version: 24.5.1, Hires upscale: 2, Hires steps: 4, Hires upscaler: ESRGAN_4x, Downcast alphas_cumprod: True, Version: 1.8.0-RC | |
| """ | |
| out_t0 = { | |
| "Positive prompt": """ | |
| masterpiece,best quality,1girl, | |
| BREAK thighhighs, | |
| BREAK (colorful spot black:1.5),color gradient, | |
| BREAK multicolored background | |
| """.strip(), | |
| "Negative prompt": "nsfw,nipples,navel,cameltoe,lowres,bad anatomy,bad hands,text,error,missing fingers,extra digit,fewer digits,cropped,worst quality,low quality,normal quality,jpeg artifacts,signature,watermark,username,blurry", | |
| "Steps": "8", | |
| "Sampler": "DPM++ 2M Karras", | |
| "CFG scale": "2", | |
| "Seed": "3627297328", | |
| "Size": "768x1024", | |
| "Model hash": "bbd321d4a3", | |
| "Model": "raemuXL_v35Lightning", | |
| "Denoising strength": "0.5", | |
| "Clip skip": "2", | |
| "ADetailer model": "face_yolov8n.pt", | |
| "ADetailer prompt": "\"black eyes, black hair, \"", | |
| "ADetailer confidence": "0.3", | |
| "ADetailer dilate erode": "4", | |
| "ADetailer mask blur": "4", | |
| "ADetailer denoising strength": "0.4", | |
| "ADetailer inpaint only masked": "True", | |
| "ADetailer inpaint padding": "32", | |
| "ADetailer version": "24.5.1", | |
| # "Hires upscale": "2", | |
| # "Hires steps": "4", | |
| # "Hires upscaler": "ESRGAN_4x", | |
| # "Downcast alphas_cumprod": "True", | |
| "Version": "1.8.0-RC" | |
| } | |
| assert self._parse(in_t0) == out_t0, "Test 0 failed" | |
| in_t1 = """ | |
| masterpiece,ultra high quality,highest quality,super fine,1girl,solo,(black background:1.3),(silhouette:1.1),sparkle,looking at viewer,upper body,simple background,glowing,(dim lighting:1.2),crystal clear,colorful clothes, | |
| Negative prompt: Easy Negative,bad handv4,ng_deepnegative_v1_75t,(worst quality:2),(low quality:2),(normal quality:2),lowres,((monochrome)),((grayscale)),bad anatomy,DeepNegative,skin spots,acnes,skin blemishes,(fat:1.2),facing away,looking away,tilted head,lowres,bad anatomy,bad hands,missing fingers,extra digit,fewer digits,bad feet,poorly drawn hands,poorly drawn face,mutation,deformed,extra fingers,extra limbs,extra arms,extra legs,malformed limbs,fused fingers,too many fingers,long neck,cross-eyed,mutated hands,polar lowres,bad body,bad proportions,gross proportions,missing arms,missing legs,extra digit,extra arms,extra leg,extra foot,teethcroppe,signature,watermark,username,blurry,cropped,jpeg artifacts,text,Lower body exposure, | |
| Steps: 30, Sampler: UniPC, Schedule type: Karras, CFG scale: 7, Seed: 3620085674, Size: 1024x1536, Model hash: 3d1b3c42ec, Model: AWPainting_v1.2, ControlNet 0: "Module: tile_resample, Model: control_v11f1e_sd15_tile_fp16 [3b860298], Weight: 0.6, Resize Mode: Crop and Resize, Processor Res: 512, Threshold A: 1.0, Threshold B: 0.5, Guidance Start: 0.0, Guidance End: 1.0, Pixel Perfect: True, Control Mode: Balanced", TI hashes: "ng_deepnegative_v1_75t: 54e7e4826d53", Pad conds: True, Version: v1.9.4 | |
| """ | |
| out_t1 = { | |
| "Positive prompt": "masterpiece,ultra high quality,highest quality,super fine,1girl,solo,(black background:1.3),(silhouette:1.1),sparkle,looking at viewer,upper body,simple background,glowing,(dim lighting:1.2),crystal clear,colorful clothes", | |
| "Negative prompt": "Easy Negative,bad handv4,ng_deepnegative_v1_75t,(worst quality:2),(low quality:2),(normal quality:2),lowres,((monochrome)),((grayscale)),bad anatomy,DeepNegative,skin spots,acnes,skin blemishes,(fat:1.2),facing away,looking away,tilted head,lowres,bad anatomy,bad hands,missing fingers,extra digit,fewer digits,bad feet,poorly drawn hands,poorly drawn face,mutation,deformed,extra fingers,extra limbs,extra arms,extra legs,malformed limbs,fused fingers,too many fingers,long neck,cross-eyed,mutated hands,polar lowres,bad body,bad proportions,gross proportions,missing arms,missing legs,extra digit,extra arms,extra leg,extra foot,teethcroppe,signature,watermark,username,blurry,cropped,jpeg artifacts,text,Lower body exposure", | |
| "Steps": "30", | |
| "Sampler": "UniPC", | |
| "Schedule type": "Karras", | |
| "CFG scale": "7", | |
| "Seed": "3620085674", | |
| "Size": "1024x1536", | |
| "Model hash": "3d1b3c42ec", | |
| "Model": "AWPainting_v1.2", | |
| "ControlNet 0": "Module: tile_resample, Model: control_v11f1e_sd15_tile_fp16 [3b860298], Weight: 0.6, Resize Mode: Crop and Resize, Processor Res: 512, Threshold A: 1.0, Threshold B: 0.5, Guidance Start: 0.0, Guidance End: 1.0, Pixel Perfect: True, Control Mode: Balanced", | |
| "TI hashes": "\"ng_deepnegative_v1_75t: 54e7e4826d53\"", | |
| # "Pad conds": "True", | |
| "Version": "v1.9.4" | |
| } | |
| assert self._parse(in_t1) == out_t1, "Test 1 failed" | |
| in_t2 = """ | |
| (official art:1.2),(colorful:1.1),(masterpiece:1.2),best quality,masterpiece,highres,original,extremely detailed wallpaper,1girl,solo,very long hair,(loli:1.3),vibrant color palette,dazzling hues,kaleidoscopic patterns,enchanting young maiden,radiant beauty,chromatic harmony,iridescent hair,sparkling eyes,lush landscapes,vivid blossoms,mesmerizing sunsets,brilliant rainbows,prismatic reflections,whimsical attire,captivating accessories,stunning chromatic display,artful composition,picturesque backdrop,breathtaking scenery,visual symphony,spellbinding chromatic enchantment, | |
| (shiny:1.2),(Oil highlights:1.2),[wet with oil:0.7],(shiny:1.2),[wet with oil:0.5], | |
| Negative prompt: (worst quality, low quality, blurry:1.5),(bad hands:1.4),watermark,(greyscale:0.88),multiple limbs,(deformed fingers, bad fingers:1.2),(ugly:1.3),monochrome,horror,geometry,bad anatomy,bad limbs,(Blurry pupil),(bad shading),error,bad composition,Extra fingers,NSFW,badhandv4,charturnerv2,corneo_dva,EasyNegative,EasyNegativeV2,ng_deepnegative_v1_75t, | |
| Steps: 25, Sampler: Euler, Schedule type: Automatic, CFG scale: 7, Seed: 848680687, Size: 1024x1536, Model hash: 099e07547a, Model: Dark Sushi Mix 大颗寿司Mix_BrighterPruned, VAE hash: f921fb3f29, VAE: kl-f8-anime2.ckpt, Denoising strength: 0.75, Clip skip: 2, Tiled Diffusion: {"Method": "MultiDiffusion", "Tile tile width": 96, "Tile tile height": 96, "Tile Overlap": 48, "Tile batch size": 4, "Keep input size": true, "NoiseInv": true, "NoiseInv Steps": 10, "NoiseInv Retouch": 1, "NoiseInv Renoise strength": 0.5, "NoiseInv Kernel size": 64}, ControlNet 0: "Module: tile_resample, Model: control_v11f1e_sd15_tile_fp16 [3b860298], Weight: 0.5, Resize Mode: Crop and Resize, Processor Res: 512, Threshold A: 1.0, Threshold B: 0.5, Guidance Start: 0.0, Guidance End: 1.0, Pixel Perfect: True, Control Mode: Balanced", Pad conds: True, Version: v1.9.4 | |
| """ | |
| out_t2 = { | |
| "Positive prompt": """ | |
| parameters(official art:1.2),(colorful:1.1),(masterpiece:1.2),best quality,masterpiece,highres,original,extremely detailed wallpaper,1girl,solo,very long hair,(loli:1.3),vibrant color palette,dazzling hues,kaleidoscopic patterns,enchanting young maiden,radiant beauty,chromatic harmony,iridescent hair,sparkling eyes,lush landscapes,vivid blossoms,mesmerizing sunsets,brilliant rainbows,prismatic reflections,whimsical attire,captivating accessories,stunning chromatic display,artful composition,picturesque backdrop,breathtaking scenery,visual symphony,spellbinding chromatic enchantment, | |
| (shiny:1.2),(Oil highlights:1.2),[wet with oil:0.7],(shiny:1.2),[wet with oil:0.5] | |
| """.strip(), | |
| "Negative prompt": "(worst quality, low quality, blurry:1.5),(bad hands:1.4),watermark,(greyscale:0.88),multiple limbs,(deformed fingers, bad fingers:1.2),(ugly:1.3),monochrome,horror,geometry,bad anatomy,bad limbs,(Blurry pupil),(bad shading),error,bad composition,Extra fingers,NSFW,badhandv4,charturnerv2,corneo_dva,EasyNegative,EasyNegativeV2,ng_deepnegative_v1_75t", | |
| "Steps": "25", | |
| "Sampler": "Euler", | |
| "Schedule type": "Automatic", | |
| "CFG scale": "7", | |
| "Seed": "848680687", | |
| "Size": "1024x1536", | |
| "Model hash": "099e07547a", | |
| "Model": "Dark Sushi Mix 大颗寿司Mix_BrighterPruned", | |
| "VAE hash": "f921fb3f29", | |
| "VAE": "kl-f8-anime2.ckpt", | |
| "Denoising strength": "0.75", | |
| "Clip skip": "2", | |
| "Tiled Diffusion": '{"Method": "MultiDiffusion", "Tile tile width": 96, "Tile tile height": 96, "Tile Overlap": 48, "Tile batch size": 4, "Keep input size": true, "NoiseInv": true, "NoiseInv Steps": 10, "NoiseInv Retouch": 1, "NoiseInv Renoise strength": 0.5, "NoiseInv Kernel size": 64}', | |
| "ControlNet 0": 'Module: tile_resample, Model: control_v11f1e_sd15_tile_fp16 [3b860298], Weight: 0.5, Resize Mode: Crop and Resize, Processor Res: 512, Threshold A: 1.0, Threshold B: 0.5, Guidance Start: 0.0, Guidance End: 1.0, Pixel Perfect: True, Control Mode: Balanced', | |
| # "Pad conds": "True", | |
| "Version": "v1.9.4", | |
| } | |
| assert self._parse(in_t2) == out_t2, "Test 2 failed" | |
| in_t3 = """ | |
| masterpiece, best quality, girl,woman,female, short hair, light smile, closed_eyes, cat_ears, overskirt,white dress,frills, pale blue Clothes,tiara | |
| Negative prompt: easynegative, ng_deepnegative_v1_75t, By bad artist -neg, verybadimagenegative_v1.3 | |
| Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 3850677924, Size: 768x1024, Model hash: 19dbfda152, Model: 二次元_mixProV45Colorbox_v45, Clip skip: 2, ENSD: 31337 | |
| """ | |
| out_t3 = { | |
| "Positive prompt": """ | |
| masterpiece, best quality, girl,woman,female, short hair, light smile, closed_eyes, cat_ears, overskirt,white dress,frills, pale blue Clothes,tiara | |
| """.strip(), | |
| "Negative prompt": "easynegative, ng_deepnegative_v1_75t, By bad artist -neg, verybadimagenegative_v1.3", | |
| "Steps": "20", | |
| "Sampler": "Euler a", | |
| "CFG scale": "7", | |
| "Seed": "3850677924", | |
| "Size": "768x1024", | |
| "Model hash": "19dbfda152", | |
| "Model": "二次元_mixProV45Colorbox_v45", | |
| "Clip skip": "2", | |
| } | |
| assert self._parse(in_t3) == out_t3, "Test 3 failed" | |
| in_t4 = """ | |
| masterpiece, best quality, 1girl, solo, voxel art, | |
| gazebo, white girl, | |
| rust hair, ochre eyes, | |
| long hair, folded ponytail, | |
| evening gown, trim dress, | |
| ribbon, Gift Hat Hair Band , Opera-length necklaces, Arm harnesses, | |
| classic, medieval, noble | |
| Negative prompt: lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry, badhandv4, easynegative, ng_deepnegative_v1_75t, verybadimagenegative_v1.3 | |
| Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 1825312441, Size: 640x960, Model hash: 149fe7d36c, Model: 二次元_meinaalter_v1, ENSD: 31337, Wildcard prompt: "masterpiece, best quality, 1girl, solo, voxel art, | |
| __scene-location__, white girl, | |
| __color__ hair, __color__ eyes, | |
| __character-hair-Size__, __character-hair-Style__, | |
| __character-clothing-Dress__, trim dress, | |
| ribbon, __character-accessories-Hair__, __character-accessories-Neck__, __character-accessories-Arm__, | |
| classic, medieval, noble" | |
| """ | |
| out_t4 = { | |
| "Positive prompt": """ | |
| masterpiece, best quality, 1girl, solo, voxel art, | |
| gazebo, white girl, | |
| rust hair, ochre eyes, | |
| long hair, folded ponytail, | |
| evening gown, trim dress, | |
| ribbon, Gift Hat Hair Band , Opera-length necklaces, Arm harnesses, | |
| classic, medieval, noble | |
| """.strip(), | |
| "Negative prompt": "lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry, badhandv4, easynegative, ng_deepnegative_v1_75t, verybadimagenegative_v1.3", | |
| "Steps": "20", | |
| "Sampler": "Euler a", | |
| "CFG scale": "7", | |
| "Seed": "1825312441", | |
| "Size": "640x960", | |
| "Model hash": "149fe7d36c", | |
| "Model": "二次元_meinaalter_v1", | |
| } | |
| assert self._parse(in_t4) == out_t4, "Test 4 failed" | |
| def base_workflow(self): | |
| wk = { | |
| "last_node_id": 11, | |
| "last_link_id": 13, | |
| "nodes": [ | |
| { | |
| "id": 7, | |
| "type": "CLIPTextEncode", | |
| "pos": [ | |
| 413, | |
| 389 | |
| ], | |
| "size": { | |
| "0": 425.27801513671875, | |
| "1": 180.6060791015625 | |
| }, | |
| "mode": 0, | |
| "inputs": [ | |
| { | |
| "name": "clip", | |
| "type": "CLIP", | |
| "link": 12, | |
| "label": "CLIP" | |
| } | |
| ], | |
| "outputs": [ | |
| { | |
| "name": "CONDITIONING", | |
| "type": "CONDITIONING", | |
| "links": [ | |
| 6 | |
| ], | |
| "slot_index": 0, | |
| "label": "条件" | |
| } | |
| ], | |
| "properties": { | |
| "Node name for S&R": "CLIPTextEncode" | |
| }, | |
| "widgets_values": [ | |
| "text, watermark" | |
| ] | |
| }, | |
| { | |
| "id": 6, | |
| "type": "CLIPTextEncode", | |
| "pos": [ | |
| 415, | |
| 186 | |
| ], | |
| "size": { | |
| "0": 422.84503173828125, | |
| "1": 164.31304931640625 | |
| }, | |
| "mode": 0, | |
| "inputs": [ | |
| { | |
| "name": "clip", | |
| "type": "CLIP", | |
| "link": 11, | |
| "label": "CLIP" | |
| } | |
| ], | |
| "outputs": [ | |
| { | |
| "name": "CONDITIONING", | |
| "type": "CONDITIONING", | |
| "links": [ | |
| 4 | |
| ], | |
| "slot_index": 0, | |
| "label": "条件" | |
| } | |
| ], | |
| "properties": { | |
| "Node name for S&R": "CLIPTextEncode" | |
| }, | |
| "widgets_values": [ | |
| "beautiful scenery nature glass bottle landscape, , purple galaxy bottle," | |
| ] | |
| }, | |
| { | |
| "id": 5, | |
| "type": "EmptyLatentImage", | |
| "pos": [ | |
| 473, | |
| 609 | |
| ], | |
| "size": { | |
| "0": 315, | |
| "1": 106 | |
| }, | |
| "mode": 0, | |
| "outputs": [ | |
| { | |
| "name": "LATENT", | |
| "type": "LATENT", | |
| "links": [ | |
| 2 | |
| ], | |
| "slot_index": 0, | |
| "label": "Latent" | |
| } | |
| ], | |
| "properties": { | |
| "Node name for S&R": "EmptyLatentImage" | |
| }, | |
| "widgets_values": [ | |
| 512, | |
| 512, | |
| 1 | |
| ] | |
| }, | |
| { | |
| "id": 3, | |
| "type": "KSampler", | |
| "pos": [ | |
| 863, | |
| 186 | |
| ], | |
| "size": { | |
| "0": 315, | |
| "1": 262 | |
| }, | |
| "mode": 0, | |
| "inputs": [ | |
| { | |
| "name": "model", | |
| "type": "MODEL", | |
| "link": 1, | |
| "label": "模型" | |
| }, | |
| { | |
| "name": "positive", | |
| "type": "CONDITIONING", | |
| "link": 4, | |
| "label": "正面条件" | |
| }, | |
| { | |
| "name": "negative", | |
| "type": "CONDITIONING", | |
| "link": 6, | |
| "label": "负面条件" | |
| }, | |
| { | |
| "name": "latent_image", | |
| "type": "LATENT", | |
| "link": 2, | |
| "label": "Latent" | |
| } | |
| ], | |
| "outputs": [ | |
| { | |
| "name": "LATENT", | |
| "type": "LATENT", | |
| "links": [ | |
| 7 | |
| ], | |
| "slot_index": 0, | |
| "label": "Latent" | |
| } | |
| ], | |
| "properties": { | |
| "Node name for S&R": "KSampler" | |
| }, | |
| "widgets_values": [ | |
| 156680208700286, | |
| "fixed", | |
| 20, | |
| 8, | |
| "euler", | |
| "normal", | |
| 1 | |
| ] | |
| }, | |
| { | |
| "id": 8, | |
| "type": "VAEDecode", | |
| "pos": [ | |
| 1209, | |
| 188 | |
| ], | |
| "size": { | |
| "0": 210, | |
| "1": 46 | |
| }, | |
| "mode": 0, | |
| "inputs": [ | |
| { | |
| "name": "samples", | |
| "type": "LATENT", | |
| "link": 7, | |
| "label": "Latent" | |
| }, | |
| { | |
| "name": "vae", | |
| "type": "VAE", | |
| "link": 8, | |
| "label": "VAE" | |
| } | |
| ], | |
| "outputs": [ | |
| { | |
| "name": "IMAGE", | |
| "type": "IMAGE", | |
| "links": [ | |
| 9, | |
| 13 | |
| ], | |
| "slot_index": 0, | |
| "label": "图像" | |
| } | |
| ], | |
| "properties": { | |
| "Node name for S&R": "VAEDecode" | |
| } | |
| }, | |
| { | |
| "id": 9, | |
| "type": "SaveImage", | |
| "pos": [ | |
| 1451, | |
| 189 | |
| ], | |
| "size": { | |
| "0": 210, | |
| "1": 58 | |
| }, | |
| "mode": 0, | |
| "inputs": [ | |
| { | |
| "name": "images", | |
| "type": "IMAGE", | |
| "link": 9, | |
| "label": "图像" | |
| } | |
| ], | |
| "properties": {}, | |
| "widgets_values": [ | |
| "ComfyUI" | |
| ] | |
| }, | |
| { | |
| "id": 4, | |
| "type": "CheckpointLoaderSimple", | |
| "pos": [ | |
| -348, | |
| 179 | |
| ], | |
| "size": { | |
| "0": 315, | |
| "1": 98 | |
| }, | |
| "mode": 0, | |
| "outputs": [ | |
| { | |
| "name": "MODEL", | |
| "type": "MODEL", | |
| "links": [ | |
| 1 | |
| ], | |
| "slot_index": 0, | |
| "label": "模型" | |
| }, | |
| { | |
| "name": "CLIP", | |
| "type": "CLIP", | |
| "links": [ | |
| 10 | |
| ], | |
| "slot_index": 1, | |
| "label": "CLIP" | |
| }, | |
| { | |
| "name": "VAE", | |
| "type": "VAE", | |
| "links": [ | |
| 8 | |
| ], | |
| "slot_index": 2, | |
| "label": "VAE" | |
| } | |
| ], | |
| "properties": { | |
| "Node name for S&R": "CheckpointLoaderSimple" | |
| }, | |
| "widgets_values": [ | |
| "mixProV4_v4.safetensors" | |
| ] | |
| }, | |
| { | |
| "id": 10, | |
| "type": "CLIPSetLastLayer", | |
| "pos": [ | |
| 17, | |
| 181 | |
| ], | |
| "size": { | |
| "0": 315, | |
| "1": 58 | |
| }, | |
| "mode": 0, | |
| "inputs": [ | |
| { | |
| "name": "clip", | |
| "type": "CLIP", | |
| "link": 10, | |
| "label": "CLIP" | |
| } | |
| ], | |
| "outputs": [ | |
| { | |
| "name": "CLIP", | |
| "type": "CLIP", | |
| "links": [ | |
| 11, | |
| 12 | |
| ], | |
| "shape": 3, | |
| "label": "CLIP", | |
| "slot_index": 0 | |
| } | |
| ], | |
| "properties": { | |
| "Node name for S&R": "CLIPSetLastLayer" | |
| }, | |
| "widgets_values": [ | |
| -1 | |
| ] | |
| }, | |
| { | |
| "id": 11, | |
| "type": "PreviewImage", | |
| "pos": [ | |
| 1450, | |
| 380 | |
| ], | |
| "size": { | |
| "0": 210, | |
| "1": 30 | |
| }, | |
| "mode": 0, | |
| "inputs": [ | |
| { | |
| "name": "images", | |
| "type": "IMAGE", | |
| "link": 13, | |
| "label": "图像" | |
| } | |
| ], | |
| "properties": { | |
| "Node name for S&R": "PreviewImage" | |
| } | |
| }, | |
| ], | |
| "links": [ | |
| [ | |
| 1, | |
| 4, | |
| 0, | |
| 3, | |
| 0, | |
| "MODEL" | |
| ], | |
| [ | |
| 2, | |
| 5, | |
| 0, | |
| 3, | |
| 3, | |
| "LATENT" | |
| ], | |
| [ | |
| 4, | |
| 6, | |
| 0, | |
| 3, | |
| 1, | |
| "CONDITIONING" | |
| ], | |
| [ | |
| 6, | |
| 7, | |
| 0, | |
| 3, | |
| 2, | |
| "CONDITIONING" | |
| ], | |
| [ | |
| 7, | |
| 3, | |
| 0, | |
| 8, | |
| 0, | |
| "LATENT" | |
| ], | |
| [ | |
| 8, | |
| 4, | |
| 2, | |
| 8, | |
| 1, | |
| "VAE" | |
| ], | |
| [ | |
| 9, | |
| 8, | |
| 0, | |
| 9, | |
| 0, | |
| "IMAGE" | |
| ], | |
| [ | |
| 10, | |
| 4, | |
| 1, | |
| 10, | |
| 0, | |
| "CLIP" | |
| ], | |
| [ | |
| 11, | |
| 10, | |
| 0, | |
| 6, | |
| 0, | |
| "CLIP" | |
| ], | |
| [ | |
| 12, | |
| 10, | |
| 0, | |
| 7, | |
| 0, | |
| "CLIP" | |
| ], | |
| [ | |
| 13, | |
| 8, | |
| 0, | |
| 11, | |
| 0, | |
| "IMAGE" | |
| ], | |
| ], | |
| "groups": [], | |
| "config": {}, | |
| "extra": { | |
| "ds": { | |
| "scale": 1.2100000000000004, | |
| "offset": [ | |
| 253.97393794242356, | |
| 53.4865032972739 | |
| ] | |
| } | |
| }, | |
| "version": 0.4 | |
| } | |
| return wk | |
| def efficient_workflow(self): | |
| wk = { | |
| "last_node_id": 5, | |
| "last_link_id": 9, | |
| "nodes": [ | |
| { | |
| "id": 4, | |
| "type": "SaveImage", | |
| "pos": [ | |
| 1040, | |
| 250 | |
| ], | |
| "size": { | |
| "0": 320, | |
| "1": 60 | |
| }, | |
| "mode": 0, | |
| "inputs": [ | |
| { | |
| "name": "images", | |
| "type": "IMAGE", | |
| "link": 8, | |
| "label": "图像" | |
| } | |
| ], | |
| "properties": { | |
| "Node name for S&R": "SaveImage" | |
| }, | |
| "widgets_values": [ | |
| "ComfyUI" | |
| ] | |
| }, | |
| { | |
| "id": 2, | |
| "type": "Efficient Loader", | |
| "pos": [ | |
| 210, | |
| 250 | |
| ], | |
| "size": { | |
| "0": 400, | |
| "1": 462 | |
| }, | |
| "mode": 0, | |
| "inputs": [ | |
| { | |
| "name": "lora_stack", | |
| "type": "LORA_STACK", | |
| "link": None, | |
| "label": "LoRA堆" | |
| }, | |
| { | |
| "name": "cnet_stack", | |
| "type": "CONTROL_NET_STACK", | |
| "link": None, | |
| "label": "ControlNet堆" | |
| } | |
| ], | |
| "outputs": [ | |
| { | |
| "name": "MODEL", | |
| "type": "MODEL", | |
| "links": [ | |
| 7 | |
| ], | |
| "shape": 3, | |
| "label": "模型", | |
| "slot_index": 0 | |
| }, | |
| { | |
| "name": "CONDITIONING+", | |
| "type": "CONDITIONING", | |
| "links": [ | |
| 3 | |
| ], | |
| "shape": 3, | |
| "label": "正面条件", | |
| "slot_index": 1 | |
| }, | |
| { | |
| "name": "CONDITIONING-", | |
| "type": "CONDITIONING", | |
| "links": [ | |
| 4 | |
| ], | |
| "shape": 3, | |
| "label": "负面条件", | |
| "slot_index": 2 | |
| }, | |
| { | |
| "name": "LATENT", | |
| "type": "LATENT", | |
| "links": [ | |
| 5 | |
| ], | |
| "shape": 3, | |
| "label": "Latent", | |
| "slot_index": 3 | |
| }, | |
| { | |
| "name": "VAE", | |
| "type": "VAE", | |
| "links": [ | |
| 6 | |
| ], | |
| "shape": 3, | |
| "label": "VAE", | |
| "slot_index": 4 | |
| }, | |
| { | |
| "name": "CLIP", | |
| "type": "CLIP", | |
| "links": None, | |
| "shape": 3, | |
| "label": "CLIP" | |
| }, | |
| { | |
| "name": "DEPENDENCIES", | |
| "type": "DEPENDENCIES", | |
| "links": None, | |
| "shape": 3, | |
| "label": "依赖" | |
| } | |
| ], | |
| "properties": { | |
| "Node name for S&R": "Efficient Loader" | |
| }, | |
| "widgets_values": [ | |
| "animagineXLV3_v30.safetensors", | |
| "Baked VAE", | |
| -1, | |
| "None", | |
| 1, | |
| 1, | |
| "CLIP_POSITIVE", | |
| "CLIP_NEGATIVE", | |
| "none", | |
| "A1111", | |
| 512, | |
| 512, | |
| 1 | |
| ], | |
| "bgcolor": "#335555", | |
| "shape": 1 | |
| }, | |
| { | |
| "id": 1, | |
| "type": "KSampler (Efficient)", | |
| "pos": [ | |
| 660, | |
| 250 | |
| ], | |
| "size": { | |
| "0": 330, | |
| "1": 370 | |
| }, | |
| "mode": 0, | |
| "inputs": [ | |
| { | |
| "name": "model", | |
| "type": "MODEL", | |
| "link": 7, | |
| "label": "模型", | |
| "slot_index": 0 | |
| }, | |
| { | |
| "name": "positive", | |
| "type": "CONDITIONING", | |
| "link": 3, | |
| "label": "正面条件" | |
| }, | |
| { | |
| "name": "negative", | |
| "type": "CONDITIONING", | |
| "link": 4, | |
| "label": "负面条件" | |
| }, | |
| { | |
| "name": "latent_image", | |
| "type": "LATENT", | |
| "link": 5, | |
| "label": "Latent" | |
| }, | |
| { | |
| "name": "optional_vae", | |
| "type": "VAE", | |
| "link": 6, | |
| "label": "VAE(可选)" | |
| }, | |
| { | |
| "name": "script", | |
| "type": "SCRIPT", | |
| "link": None, | |
| "label": "脚本" | |
| } | |
| ], | |
| "outputs": [ | |
| { | |
| "name": "MODEL", | |
| "type": "MODEL", | |
| "links": None, | |
| "shape": 3, | |
| "label": "模型" | |
| }, | |
| { | |
| "name": "CONDITIONING+", | |
| "type": "CONDITIONING", | |
| "links": None, | |
| "shape": 3, | |
| "label": "正面条件" | |
| }, | |
| { | |
| "name": "CONDITIONING-", | |
| "type": "CONDITIONING", | |
| "links": None, | |
| "shape": 3, | |
| "label": "负面条件" | |
| }, | |
| { | |
| "name": "LATENT", | |
| "type": "LATENT", | |
| "links": None, | |
| "shape": 3, | |
| "label": "Latent" | |
| }, | |
| { | |
| "name": "VAE", | |
| "type": "VAE", | |
| "links": None, | |
| "shape": 3, | |
| "label": "VAE" | |
| }, | |
| { | |
| "name": "IMAGE", | |
| "type": "IMAGE", | |
| "links": [ | |
| 8, | |
| 9 | |
| ], | |
| "shape": 3, | |
| "label": "图像", | |
| "slot_index": 5 | |
| } | |
| ], | |
| "properties": { | |
| "Node name for S&R": "KSampler (Efficient)" | |
| }, | |
| "widgets_values": [ | |
| 800315283332510, | |
| "fixed", | |
| 20, | |
| 7, | |
| "euler", | |
| "normal", | |
| 1, | |
| "auto", | |
| "true" | |
| ], | |
| "bgcolor": "#333355", | |
| "shape": 1 | |
| }, | |
| { | |
| "id": 5, | |
| "type": "PreviewImage", | |
| "pos": [ | |
| 1050, | |
| 520 | |
| ], | |
| "size": { | |
| "0": 210, | |
| "1": 30 | |
| }, | |
| "mode": 0, | |
| "inputs": [ | |
| { | |
| "name": "images", | |
| "type": "IMAGE", | |
| "link": 9, | |
| "label": "图像" | |
| } | |
| ], | |
| "properties": { | |
| "Node name for S&R": "PreviewImage" | |
| } | |
| } | |
| ], | |
| "links": [ | |
| [ | |
| 3, | |
| 2, | |
| 1, | |
| 1, | |
| 1, | |
| "CONDITIONING" | |
| ], | |
| [ | |
| 4, | |
| 2, | |
| 2, | |
| 1, | |
| 2, | |
| "CONDITIONING" | |
| ], | |
| [ | |
| 5, | |
| 2, | |
| 3, | |
| 1, | |
| 3, | |
| "LATENT" | |
| ], | |
| [ | |
| 6, | |
| 2, | |
| 4, | |
| 1, | |
| 4, | |
| "VAE" | |
| ], | |
| [ | |
| 7, | |
| 2, | |
| 0, | |
| 1, | |
| 0, | |
| "MODEL" | |
| ], | |
| [ | |
| 8, | |
| 1, | |
| 5, | |
| 4, | |
| 0, | |
| "IMAGE" | |
| ], | |
| [ | |
| 9, | |
| 1, | |
| 5, | |
| 5, | |
| 0, | |
| "IMAGE" | |
| ] | |
| ], | |
| "groups": [], | |
| "config": {}, | |
| "extra": { | |
| "ds": { | |
| "scale": 1.2100000000000006, | |
| "offset": [ | |
| -639.1956340693308, | |
| -38.20042379820701 | |
| ] | |
| } | |
| }, | |
| "version": 0.4 | |
| } | |
| return wk | |
| if __name__ == "__main__": | |
| webui_parser = WebUIToComfyUI("") | |
| webui_parser.test() | |
Xet Storage Details
- Size:
- 104 kB
- Xet hash:
- d14e4e67fd1f5cd513cc94e2a2cf080f7a5aa50b91eccd1e6a058b53c6fb8266
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.