├── .github └── workflows │ └── publish.yml ├── .gitignore ├── README.MD ├── README_CN.MD ├── __init__.py ├── dzNodes.json ├── dzNodes.py ├── font └── Alibaba-PuHuiTi-Heavy.ttf ├── font_dir.ini ├── image ├── comfy_wordcloud_advance.png ├── comfy_wordcloud_simple.png ├── load_text_file.png ├── rgb_color_picker.png ├── word_cloud.png └── word_cloud_mask.png ├── mtb ├── dz_comfy_shared.js ├── dz_debug.js ├── dz_mtb_widgets.js └── dz_parse-css.js ├── node.tar.gz ├── py ├── __pycache__ │ ├── comfy_wordcloud.cpython-311.pyc │ ├── load_textfile.cpython-311.pyc │ └── rgb_picker.cpython-311.pyc ├── comfy_wordcloud.py ├── load_textfile.py └── rgb_picker.py ├── pyproject.toml ├── requirements.txt └── workflow ├── comfy_wordcloud_advance.json └── comfy_wordcloud_simple.json /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Comfy registry 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "pyproject.toml" 9 | 10 | permissions: 11 | issues: write 12 | 13 | jobs: 14 | publish-node: 15 | name: Publish Custom Node to registry 16 | runs-on: ubuntu-latest 17 | if: ${{ github.repository_owner == 'chflame163' }} 18 | steps: 19 | - name: Check out code 20 | uses: actions/checkout@v4 21 | - name: Publish Custom Node 22 | uses: Comfy-Org/publish-node-action@v1 23 | with: 24 | ## Add your own personal access token to your Github Repository secrets and reference it here. 25 | personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _test_*.* 2 | __pycache__ 3 | .venv 4 | .idea 5 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # ComfyUI Word Cloud: 2 | A plugin to generating word cloud images for ComfyUI, it is the implementation of the [word_cloud](https://github.com/amueller/word_cloud) on ComfyUI. 3 | ![image](image/comfy_wordcloud_advance.png) 4 | 5 | [中文说明点这里](./README_CN.MD) 6 | 7 | ## Update: 8 | * Word Cloud node add mask output. 9 | ![image](image/word_cloud_mask.png) 10 | 11 | * Add RGB Color Picker node that makes color selection more convenient. 12 | 13 | * By editing the font_dir.ini, located in the root directory of the plugin, users can customize the font directory. Every time comfyUI is launched, the *.ttf and *.otf files 14 | in this directory will be collected and displayed in the plugin font_path option. 15 | font_dir.ini defaults to the Windows system font directory (C:\Windows\fonts). 16 | If the custom directory is invalid, the built-in font directory will be enabled, 17 | This directory contains Alibaba-PuHuiTi-Heavy.ttf file, which belongs to Alibaba (China) Co., Ltd. and is free for use by any individual or enterprise. 18 | 19 | 20 | ### Important reminder: The font needs to be reset for the old version nodes saved in the workflow before loading. 21 | * Set the font_dir.ini, and start comfyUI to load workflow, in the font_path of the WordCloud node, reselect the font. 22 | 23 | * This update is based on [ZHO-ZHO-ZHO](https://github.com/ZHO-ZHO-ZHO/ComfyUI-Text_Image-Composite)'s suggestions and assistance. 24 | 25 | 26 | ## Node Description 27 | 28 | ### Word Cloud: 29 | ![image](image/word_cloud.png) 30 | Generate word cloud images based on text content, where word size is related to word frequency, and the higher the frequency, the larger the text. Can define color schemes, set key words, set exclusion words, etc. Support generating word cloud images with contour shapes by inputting images with alpha channels. 31 | 32 | Node options: 33 | * color_ref_image: The input image serves as a reference for text color. 34 | * mask_image: The input image serves as the silhouette of the text shape. If there with alpha channel, use the channel as the contour; The shape of the contour is determined by color without channels, and the white part will be excluded. 35 | * text: The text content here will be broken down into individual words, which serve as elements of the word cloud. 36 | * width: Generate the width of the image. (If there is mask_image input, use the size of mask_image, and this setting value is ignored) 37 | * height: The height of the generated image. (If there is mask_image input, use the size of mask_image, and this setting value is ignored) 38 | * scale: The amplification factor, the final generated image size will be the width and height set values multiplied by this number. 39 | * margin: Blank edge size. 40 | * font_path: font file. 41 | * min_font_size: The minimum value displayed for word elements. 42 | * max_font_size:The maximum value displayed for word elements. 43 | * relative_scaling: The relative size of word elements in the word cloud. The larger the value, the higher the dispersion. 44 | * colormap: Text color. multiple predefined colormaps provided by Matplotlib are used. (If there is input of color_ref_image, this setting is ignored) 45 | * background_color: Background color, described in hexadecimal RGB format. (If transparent background is set to True, this setting is ignored) 46 | * transparent_background: Is the background transparent. Set True here to output images with alpha channels. 47 | * prefer_horizontal: Word level occurrence rate. Minimum 0 (all vertical rows), maximum 1 (all horizontal rows). 48 | * max_words: Maximum number of words. 49 | * repeat: Is it allowed to repeat when the maximum number of words is not reached. 50 | * include_numbers: Does the word element contain numbers. 51 | * random_state:Seed of the random number generator during the word cloud generation process. Set -1 to be random each time, while other values are fixed each time. 52 | * stopwords: The words set here will not appear in the picture. Separate each word with a comma (both in Chinese and English) or a space. 53 | * contour_width: Outline width of silhouette. With a mask_ Image input is only valid. 54 | * contour_color: Color of Outline. With a mask_ Image input is only valid. 55 | * keynote_words: The words set here will be further enlarged, except for those with the same words set in stopwords. Separate each word with a comma (both in Chinese and English) or a space. 56 | * keynote_weight: Weighted key for keynote words. The larger the value, the relatively larger the key words. 57 | 58 | Output Type: 59 | * image(support alpha channel) 60 | * mask 61 | 62 | ### RGB Color Picker 63 | ![image](image/rgb_color_picker.png) 64 | Modify web extensions from [mtb nodes](https://github.com/melMass/comfy_mtb). Select colors on the color palette and output RGB values. 65 | 66 | 67 | Node options: 68 | * mode: The output format is available in hexadecimal (HEX) and decimal (DEC). 69 | 70 | Output Type: 71 | * string 72 | 73 | ### Load Text File: 74 | ![image](image/load_text_file.png) 75 | Load a text file from the specified path and set to use UTF-8 encoding. 76 | 77 | Options: 78 | * path: Pathname for .txt file。 79 | 80 | Output Type: 81 | * string 82 | 83 | ## Example workflow 84 | 85 | ![image](image/comfy_wordcloud_simple.png) 86 | Some JSON workflow files in the workflow directory, that is example for ComfyUI. 87 | 88 | ## How to install 89 | 90 | * Open the cmd window in the plugin directory of ComfyUI, like "ComfyUI\custom_nodes\",type```git clone https://github.com/chflame163/ComfyUI_WordCloud.git``` 91 | or download the zip file and extracted, copy the resulting folder to ComfyUI\custom_ Nodes\ 92 | 93 | * Install dependency packages, open the cmd window in the WordCloud plugin directory like "ComfyUI\custom_ Nodes\ComfyUI_WordCloud" and enter the following command: 94 | ```..\..\..\python_embeded\python.exe -m pip install -r requirements.txt``` 95 | 96 | * Restart ComfyUI 97 | -------------------------------------------------------------------------------- /README_CN.MD: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chflame163/ComfyUI_WordCloud/9a20688fae496858f6c0e3c4bf591a3fa5198c9f/README_CN.MD -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | import importlib.util 2 | import glob 3 | import os 4 | import sys 5 | import filecmp 6 | import shutil 7 | import __main__ 8 | from .dzNodes import init, get_ext_dir, log 9 | 10 | NODE_CLASS_MAPPINGS = {} 11 | NODE_DISPLAY_NAME_MAPPINGS = {} 12 | 13 | python = sys.executable 14 | extentions_folder = os.path.join(os.path.dirname(os.path.realpath(__main__.__file__)), 15 | "web" + os.sep + "extensions" + os.sep + "mtb") 16 | javascript_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), "mtb") 17 | outdate_file_list = ['comfy_shared.js', 'debug.js', 'mtb_widgets.js', 'parse-css.js', 'dz_widgets.js'] 18 | 19 | if not os.path.exists(extentions_folder): 20 | log('Making the "web\extensions\mtb" folder') 21 | os.mkdir(extentions_folder) 22 | else: 23 | for i in outdate_file_list: 24 | outdate_file = os.path.join(extentions_folder, i) 25 | if os.path.exists(outdate_file): 26 | os.remove(outdate_file) 27 | 28 | result = filecmp.dircmp(javascript_folder, extentions_folder) 29 | 30 | if result.left_only or result.diff_files: 31 | log('Update to javascripts files detected') 32 | file_list = list(result.left_only) 33 | file_list.extend(x for x in result.diff_files if x not in file_list) 34 | 35 | for file in file_list: 36 | log(f'WordCloud: Copying {file} to extensions folder') 37 | src_file = os.path.join(javascript_folder, file) 38 | dst_file = os.path.join(extentions_folder, file) 39 | if os.path.exists(dst_file): 40 | os.remove(dst_file) 41 | #log("disabled") 42 | shutil.copy(src_file, dst_file) 43 | 44 | if init(): 45 | py = get_ext_dir("py") 46 | files = glob.glob("*.py", root_dir=py, recursive=False) 47 | for file in files: 48 | name = os.path.splitext(file)[0] 49 | spec = importlib.util.spec_from_file_location(name, os.path.join(py, file)) 50 | module = importlib.util.module_from_spec(spec) 51 | sys.modules[name] = module 52 | spec.loader.exec_module(module) 53 | if hasattr(module, "NODE_CLASS_MAPPINGS") and getattr(module, "NODE_CLASS_MAPPINGS") is not None: 54 | NODE_CLASS_MAPPINGS.update(module.NODE_CLASS_MAPPINGS) 55 | if hasattr(module, "NODE_DISPLAY_NAME_MAPPINGS") and getattr(module, "NODE_DISPLAY_NAME_MAPPINGS") is not None: 56 | NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS) 57 | 58 | __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] 59 | -------------------------------------------------------------------------------- /dzNodes.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dzNodes", 3 | "logging": false 4 | } -------------------------------------------------------------------------------- /dzNodes.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import json 4 | import shutil 5 | import inspect 6 | import aiohttp 7 | from server import PromptServer 8 | from tqdm import tqdm 9 | 10 | config = None 11 | 12 | 13 | def is_logging_enabled(): 14 | config = get_extension_config() 15 | if "logging" not in config: 16 | return False 17 | return config["logging"] 18 | 19 | 20 | def log(message, type=None, always=False, name=None): 21 | if not always and not is_logging_enabled(): 22 | return 23 | 24 | if type is not None: 25 | message = f"[{type}] {message}" 26 | 27 | if name is None: 28 | name = get_extension_config()["name"] 29 | 30 | print(f"# 😺dzNodes: {name} -> {message}") 31 | 32 | 33 | def get_ext_dir(subpath=None, mkdir=False): 34 | dir = os.path.dirname(__file__) 35 | if subpath is not None: 36 | dir = os.path.join(dir, subpath) 37 | 38 | dir = os.path.abspath(dir) 39 | 40 | if mkdir and not os.path.exists(dir): 41 | os.makedirs(dir) 42 | return dir 43 | 44 | 45 | def get_comfy_dir(subpath=None, mkdir=False): 46 | dir = os.path.dirname(inspect.getfile(PromptServer)) 47 | if subpath is not None: 48 | dir = os.path.join(dir, subpath) 49 | 50 | dir = os.path.abspath(dir) 51 | 52 | if mkdir and not os.path.exists(dir): 53 | os.makedirs(dir) 54 | return dir 55 | 56 | 57 | def get_web_ext_dir(): 58 | config = get_extension_config() 59 | name = config["name"] 60 | dir = get_comfy_dir("web/extensions/dzNodes") 61 | if not os.path.exists(dir): 62 | os.makedirs(dir) 63 | dir = os.path.join(dir, name) 64 | return dir 65 | 66 | 67 | def get_extension_config(reload=False): 68 | global config 69 | if reload == False and config is not None: 70 | return config 71 | 72 | config_path = get_ext_dir("dzNodes.json") 73 | if not os.path.exists(config_path): 74 | log("Missing json, this extension may not work correctly. Please reinstall the extension.", 75 | type="ERROR", always=True, name="???") 76 | print(f"Extension path: {get_ext_dir()}") 77 | return {"name": "Unknown", "version": -1} 78 | with open(config_path, "r") as f: 79 | config = json.loads(f.read()) 80 | return config 81 | 82 | 83 | def link_js(src, dst): 84 | src = os.path.abspath(src) 85 | dst = os.path.abspath(dst) 86 | if os.name == "nt": 87 | try: 88 | import _winapi 89 | _winapi.CreateJunction(src, dst) 90 | return True 91 | except: 92 | pass 93 | try: 94 | os.symlink(src, dst) 95 | return True 96 | except: 97 | import logging 98 | logging.exception('') 99 | return False 100 | 101 | def is_junction(path): 102 | if os.name != "nt": 103 | return False 104 | try: 105 | return bool(os.readlink(path)) 106 | except OSError: 107 | return False 108 | 109 | def install_js(): 110 | src_dir = get_ext_dir("js") 111 | if not os.path.exists(src_dir): 112 | log("No JS") 113 | return 114 | 115 | dst_dir = get_web_ext_dir() 116 | 117 | if os.path.exists(dst_dir): 118 | if os.path.islink(dst_dir) or is_junction(dst_dir): 119 | log("JS already linked") 120 | return 121 | elif link_js(src_dir, dst_dir): 122 | log("JS linked") 123 | return 124 | 125 | log("Copying JS files") 126 | shutil.copytree(src_dir, dst_dir, dirs_exist_ok=True) 127 | 128 | 129 | def init(check_imports=None): 130 | log("Init") 131 | 132 | if check_imports is not None: 133 | import importlib.util 134 | for imp in check_imports: 135 | spec = importlib.util.find_spec(imp) 136 | if spec is None: 137 | log(f"{imp} is required, please check requirements are installed.", 138 | type="ERROR", always=True) 139 | return False 140 | 141 | install_js() 142 | return True 143 | 144 | 145 | def get_async_loop(): 146 | loop = None 147 | try: 148 | loop = asyncio.get_event_loop() 149 | except: 150 | loop = asyncio.new_event_loop() 151 | asyncio.set_event_loop(loop) 152 | return loop 153 | 154 | 155 | def get_http_session(): 156 | loop = get_async_loop() 157 | return aiohttp.ClientSession(loop=loop) 158 | 159 | 160 | async def download(url, stream, update_callback=None, session=None): 161 | close_session = False 162 | if session is None: 163 | close_session = True 164 | session = get_http_session() 165 | try: 166 | async with session.get(url) as response: 167 | size = int(response.headers.get('content-length', 0)) or None 168 | 169 | with tqdm( 170 | unit='B', unit_scale=True, miniters=1, desc=url.split('/')[-1], total=size, 171 | ) as progressbar: 172 | perc = 0 173 | async for chunk in response.content.iter_chunked(2048): 174 | stream.write(chunk) 175 | progressbar.update(len(chunk)) 176 | if update_callback is not None and progressbar.total is not None and progressbar.total != 0: 177 | last = perc 178 | perc = round(progressbar.n / progressbar.total, 2) 179 | if perc != last: 180 | last = perc 181 | await update_callback(perc) 182 | finally: 183 | if close_session and session is not None: 184 | await session.close() 185 | 186 | 187 | async def download_to_file(url, destination, update_callback=None, is_ext_subpath=True, session=None): 188 | if is_ext_subpath: 189 | destination = get_ext_dir(destination) 190 | with open(destination, mode='wb') as f: 191 | download(url, f, update_callback, session) 192 | 193 | 194 | def wait_for_async(async_fn, loop=None): 195 | res = [] 196 | 197 | async def run_async(): 198 | r = await async_fn() 199 | res.append(r) 200 | 201 | if loop is None: 202 | try: 203 | loop = asyncio.get_event_loop() 204 | except: 205 | loop = asyncio.new_event_loop() 206 | asyncio.set_event_loop(loop) 207 | 208 | loop.run_until_complete(run_async()) 209 | 210 | return res[0] 211 | 212 | 213 | def update_node_status(client_id, node, text, progress=None): 214 | if client_id is None: 215 | client_id = PromptServer.instance.client_id 216 | 217 | if client_id is None: 218 | return 219 | 220 | PromptServer.instance.send_sync("dzNodes/update_status", { 221 | "node": node, 222 | "progress": progress, 223 | "text": text 224 | }, client_id) 225 | 226 | 227 | async def update_node_status_async(client_id, node, text, progress=None): 228 | if client_id is None: 229 | client_id = PromptServer.instance.client_id 230 | 231 | if client_id is None: 232 | return 233 | 234 | await PromptServer.instance.send("dzNodes/update_status", { 235 | "node": node, 236 | "progress": progress, 237 | "text": text 238 | }, client_id) 239 | 240 | 241 | def get_config_value(key, default=None, throw=False): 242 | split = key.split(".") 243 | obj = get_extension_config() 244 | for s in split: 245 | if s in split: 246 | obj = obj[s] 247 | else: 248 | if throw: 249 | raise KeyError("Configuration key missing: " + key) 250 | else: 251 | return default 252 | return obj 253 | 254 | 255 | def is_inside_dir(root_dir, check_path): 256 | root_dir = os.path.abspath(root_dir) 257 | if not os.path.isabs(check_path): 258 | check_path = os.path.abspath(os.path.join(root_dir, check_path)) 259 | return os.path.commonpath([check_path, root_dir]) == root_dir 260 | 261 | 262 | def get_child_dir(root_dir, child_path, throw_if_outside=True): 263 | child_path = os.path.abspath(os.path.join(root_dir, child_path)) 264 | if is_inside_dir(root_dir, child_path): 265 | return child_path 266 | if throw_if_outside: 267 | raise NotADirectoryError( 268 | "Saving outside the target folder is not allowed.") 269 | return None 270 | -------------------------------------------------------------------------------- /font/Alibaba-PuHuiTi-Heavy.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chflame163/ComfyUI_WordCloud/9a20688fae496858f6c0e3c4bf591a3fa5198c9f/font/Alibaba-PuHuiTi-Heavy.ttf -------------------------------------------------------------------------------- /font_dir.ini: -------------------------------------------------------------------------------- 1 | font_dir=C:\Windows\fonts 2 | -------------------------------------------------------------------------------- /image/comfy_wordcloud_advance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chflame163/ComfyUI_WordCloud/9a20688fae496858f6c0e3c4bf591a3fa5198c9f/image/comfy_wordcloud_advance.png -------------------------------------------------------------------------------- /image/comfy_wordcloud_simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chflame163/ComfyUI_WordCloud/9a20688fae496858f6c0e3c4bf591a3fa5198c9f/image/comfy_wordcloud_simple.png -------------------------------------------------------------------------------- /image/load_text_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chflame163/ComfyUI_WordCloud/9a20688fae496858f6c0e3c4bf591a3fa5198c9f/image/load_text_file.png -------------------------------------------------------------------------------- /image/rgb_color_picker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chflame163/ComfyUI_WordCloud/9a20688fae496858f6c0e3c4bf591a3fa5198c9f/image/rgb_color_picker.png -------------------------------------------------------------------------------- /image/word_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chflame163/ComfyUI_WordCloud/9a20688fae496858f6c0e3c4bf591a3fa5198c9f/image/word_cloud.png -------------------------------------------------------------------------------- /image/word_cloud_mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chflame163/ComfyUI_WordCloud/9a20688fae496858f6c0e3c4bf591a3fa5198c9f/image/word_cloud_mask.png -------------------------------------------------------------------------------- /mtb/dz_comfy_shared.js: -------------------------------------------------------------------------------- 1 | /** 2 | * File: dz_comfy_shared.js 3 | * Author: Mel Massadian 4 | * 5 | * Copyright (c) 2023 Mel Massadian 6 | * 7 | */ 8 | 9 | import { app } from '../../scripts/app.js' 10 | 11 | export const log = (...args) => { 12 | if (window.DZ?.DEBUG) { 13 | console.debug(...args) 14 | } 15 | } 16 | 17 | //- WIDGET UTILS 18 | export const CONVERTED_TYPE = 'converted-widget' 19 | 20 | export const hasWidgets = (node) => { 21 | if (!node.widgets || !node.widgets?.[Symbol.iterator]) { 22 | return false 23 | } 24 | return true 25 | } 26 | 27 | export const cleanupNode = (node) => { 28 | if (!hasWidgets(node)) { 29 | return 30 | } 31 | 32 | for (const w of node.widgets) { 33 | if (w.canvas) { 34 | w.canvas.remove() 35 | } 36 | if (w.inputEl) { 37 | w.inputEl.remove() 38 | } 39 | // calls the widget remove callback 40 | w.onRemoved?.() 41 | } 42 | } 43 | 44 | export function offsetDOMWidget( 45 | widget, 46 | ctx, 47 | node, 48 | widgetWidth, 49 | widgetY, 50 | height 51 | ) { 52 | const margin = 10 53 | const elRect = ctx.canvas.getBoundingClientRect() 54 | const transform = new DOMMatrix() 55 | .scaleSelf( 56 | elRect.width / ctx.canvas.width, 57 | elRect.height / ctx.canvas.height 58 | ) 59 | .multiplySelf(ctx.getTransform()) 60 | .translateSelf(margin, margin + widgetY) 61 | 62 | const scale = new DOMMatrix().scaleSelf(transform.a, transform.d) 63 | Object.assign(widget.inputEl.style, { 64 | transformOrigin: '0 0', 65 | transform: scale, 66 | left: `${transform.a + transform.e}px`, 67 | top: `${transform.d + transform.f}px`, 68 | width: `${widgetWidth - margin * 2}px`, 69 | // height: `${(widget.parent?.inputHeight || 32) - (margin * 2)}px`, 70 | height: `${(height || widget.parent?.inputHeight || 32) - margin * 2}px`, 71 | 72 | position: 'absolute', 73 | background: !node.color ? '' : node.color, 74 | color: !node.color ? '' : 'white', 75 | zIndex: 5, //app.graph._nodes.indexOf(node), 76 | }) 77 | } 78 | 79 | /** 80 | * Extracts the type and link type from a widget config object. 81 | * @param {*} config 82 | * @returns 83 | */ 84 | export function getWidgetType(config) { 85 | // Special handling for COMBO so we restrict links based on the entries 86 | let type = config?.[0] 87 | let linkType = type 88 | if (type instanceof Array) { 89 | type = 'COMBO' 90 | linkType = linkType.join(',') 91 | } 92 | return { type, linkType } 93 | } 94 | 95 | export const dynamic_connection = ( 96 | node, 97 | index, 98 | connected, 99 | connectionPrefix = 'input_', 100 | connectionType = 'PSDLAYER' 101 | ) => { 102 | // remove all non connected inputs 103 | if (!connected && node.inputs.length > 1) { 104 | log(`Removing input ${index} (${node.inputs[index].name})`) 105 | if (node.widgets) { 106 | const w = node.widgets.find((w) => w.name === node.inputs[index].name) 107 | if (w) { 108 | w.onRemoved?.() 109 | node.widgets.length = node.widgets.length - 1 110 | } 111 | } 112 | node.removeInput(index) 113 | 114 | // make inputs sequential again 115 | for (let i = 0; i < node.inputs.length; i++) { 116 | node.inputs[i].label = `${connectionPrefix}${i + 1}` 117 | } 118 | } 119 | 120 | // add an extra input 121 | if (node.inputs[node.inputs.length - 1].link != undefined) { 122 | log( 123 | `Adding input ${node.inputs.length + 1} (${connectionPrefix}${ 124 | node.inputs.length + 1 125 | })` 126 | ) 127 | 128 | node.addInput( 129 | `${connectionPrefix}${node.inputs.length + 1}`, 130 | connectionType 131 | ) 132 | } 133 | } 134 | 135 | /** 136 | * Appends a callback to the extra menu options of a given node type. 137 | * @param {*} nodeType 138 | * @param {*} cb 139 | */ 140 | export function addMenuHandler(nodeType, cb) { 141 | const getOpts = nodeType.prototype.getExtraMenuOptions 142 | nodeType.prototype.getExtraMenuOptions = function () { 143 | const r = getOpts.apply(this, arguments) 144 | cb.apply(this, arguments) 145 | return r 146 | } 147 | } 148 | 149 | export function hideWidget(node, widget, suffix = '') { 150 | widget.origType = widget.type 151 | widget.hidden = true 152 | widget.origComputeSize = widget.computeSize 153 | widget.origSerializeValue = widget.serializeValue 154 | widget.computeSize = () => [0, -4] // -4 is due to the gap litegraph adds between widgets automatically 155 | widget.type = CONVERTED_TYPE + suffix 156 | widget.serializeValue = () => { 157 | // Prevent serializing the widget if we have no input linked 158 | const { link } = node.inputs.find((i) => i.widget?.name === widget.name) 159 | if (link == null) { 160 | return undefined 161 | } 162 | return widget.origSerializeValue 163 | ? widget.origSerializeValue() 164 | : widget.value 165 | } 166 | 167 | // Hide any linked widgets, e.g. seed+seedControl 168 | if (widget.linkedWidgets) { 169 | for (const w of widget.linkedWidgets) { 170 | hideWidget(node, w, ':' + widget.name) 171 | } 172 | } 173 | } 174 | 175 | export function showWidget(widget) { 176 | widget.type = widget.origType 177 | widget.computeSize = widget.origComputeSize 178 | widget.serializeValue = widget.origSerializeValue 179 | 180 | delete widget.origType 181 | delete widget.origComputeSize 182 | delete widget.origSerializeValue 183 | 184 | // Hide any linked widgets, e.g. seed+seedControl 185 | if (widget.linkedWidgets) { 186 | for (const w of widget.linkedWidgets) { 187 | showWidget(w) 188 | } 189 | } 190 | } 191 | 192 | export function convertToWidget(node, widget) { 193 | showWidget(widget) 194 | const sz = node.size 195 | node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name)) 196 | 197 | for (const widget of node.widgets) { 198 | widget.last_y -= LiteGraph.NODE_SLOT_HEIGHT 199 | } 200 | 201 | // Restore original size but grow if needed 202 | node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]) 203 | } 204 | 205 | export function convertToInput(node, widget, config) { 206 | hideWidget(node, widget) 207 | 208 | const { linkType } = getWidgetType(config) 209 | 210 | // Add input and store widget config for creating on primitive node 211 | const sz = node.size 212 | node.addInput(widget.name, linkType, { 213 | widget: { name: widget.name, config }, 214 | }) 215 | 216 | for (const widget of node.widgets) { 217 | widget.last_y += LiteGraph.NODE_SLOT_HEIGHT 218 | } 219 | 220 | // Restore original size but grow if needed 221 | node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]) 222 | } 223 | 224 | export function hideWidgetForGood(node, widget, suffix = '') { 225 | widget.origType = widget.type 226 | widget.origComputeSize = widget.computeSize 227 | widget.origSerializeValue = widget.serializeValue 228 | widget.computeSize = () => [0, -4] // -4 is due to the gap litegraph adds between widgets automatically 229 | widget.type = CONVERTED_TYPE + suffix 230 | // widget.serializeValue = () => { 231 | // // Prevent serializing the widget if we have no input linked 232 | // const w = node.inputs?.find((i) => i.widget?.name === widget.name); 233 | // if (w?.link == null) { 234 | // return undefined; 235 | // } 236 | // return widget.origSerializeValue ? widget.origSerializeValue() : widget.value; 237 | // }; 238 | 239 | // Hide any linked widgets, e.g. seed+seedControl 240 | if (widget.linkedWidgets) { 241 | for (const w of widget.linkedWidgets) { 242 | hideWidgetForGood(node, w, ':' + widget.name) 243 | } 244 | } 245 | } 246 | 247 | export function fixWidgets(node) { 248 | if (node.inputs) { 249 | for (const input of node.inputs) { 250 | log(input) 251 | if (input.widget || node.widgets) { 252 | // if (newTypes.includes(input.type)) { 253 | const matching_widget = node.widgets.find((w) => w.name === input.name) 254 | if (matching_widget) { 255 | // if (matching_widget.hidden) { 256 | // log(`Already hidden skipping ${matching_widget.name}`) 257 | // continue 258 | // } 259 | const w = node.widgets.find((w) => w.name === matching_widget.name) 260 | if (w && w.type != CONVERTED_TYPE) { 261 | log(w) 262 | log(`hidding ${w.name}(${w.type}) from ${node.type}`) 263 | log(node) 264 | hideWidget(node, w) 265 | } else { 266 | log(`converting to widget ${w}`) 267 | 268 | convertToWidget(node, input) 269 | } 270 | } 271 | } 272 | } 273 | } 274 | } 275 | export function inner_value_change(widget, value, event = undefined) { 276 | if (widget.type == 'number' || widget.type == 'BBOX') { 277 | value = Number(value) 278 | } else if (widget.type == 'BOOL') { 279 | value = Boolean(value) 280 | } 281 | widget.value = value 282 | if ( 283 | widget.options && 284 | widget.options.property && 285 | node.properties[widget.options.property] !== undefined 286 | ) { 287 | node.setProperty(widget.options.property, value) 288 | } 289 | if (widget.callback) { 290 | widget.callback(widget.value, app.canvas, node, pos, event) 291 | } 292 | } 293 | 294 | //- COLOR UTILS 295 | export function isColorBright(rgb, threshold = 240) { 296 | const brightess = getBrightness(rgb) 297 | return brightess > threshold 298 | } 299 | 300 | function getBrightness(rgbObj) { 301 | return Math.round( 302 | (parseInt(rgbObj[0]) * 299 + 303 | parseInt(rgbObj[1]) * 587 + 304 | parseInt(rgbObj[2]) * 114) / 305 | 1000 306 | ) 307 | } 308 | 309 | //- HTML / CSS UTILS 310 | export function defineClass(className, classStyles) { 311 | const styleSheets = document.styleSheets 312 | 313 | // Helper function to check if the class exists in a style sheet 314 | function classExistsInStyleSheet(styleSheet) { 315 | const rules = styleSheet.rules || styleSheet.cssRules 316 | for (const rule of rules) { 317 | if (rule.selectorText === `.${className}`) { 318 | return true 319 | } 320 | } 321 | return false 322 | } 323 | 324 | // Check if the class is already defined in any of the style sheets 325 | let classExists = false 326 | for (const styleSheet of styleSheets) { 327 | if (classExistsInStyleSheet(styleSheet)) { 328 | classExists = true 329 | break 330 | } 331 | } 332 | 333 | // If the class doesn't exist, add the new class definition to the first style sheet 334 | if (!classExists) { 335 | if (styleSheets[0].insertRule) { 336 | styleSheets[0].insertRule(`.${className} { ${classStyles} }`, 0) 337 | } else if (styleSheets[0].addRule) { 338 | styleSheets[0].addRule(`.${className}`, classStyles, 0) 339 | } 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /mtb/dz_debug.js: -------------------------------------------------------------------------------- 1 | /** 2 | * File: debug.js 3 | * Project: comfy_DZ 4 | * Author: Mel Massadian 5 | * 6 | * Copyright (c) 2023 Mel Massadian 7 | * 8 | */ 9 | 10 | import { app } from '../../scripts/app.js' 11 | 12 | import * as shared from './dz_comfy_shared.js' 13 | import { log } from './dz_comfy_shared.js' 14 | import { DZWidgets } from './dz_DZ_widgets.js' 15 | 16 | // TODO: respect inputs order... 17 | 18 | function escapeHtml(unsafe) { 19 | return unsafe 20 | .replace(/&/g, '&') 21 | .replace(//g, '>') 23 | .replace(/"/g, '"') 24 | .replace(/'/g, ''') 25 | } 26 | app.registerExtension({ 27 | name: 'DZ.Debug', 28 | async beforeRegisterNodeDef(nodeType, nodeData, app) { 29 | if (nodeData.name === 'Debug (DZ)') { 30 | const onConnectionsChange = nodeType.prototype.onConnectionsChange 31 | nodeType.prototype.onConnectionsChange = function ( 32 | type, 33 | index, 34 | connected, 35 | link_info 36 | ) { 37 | const r = onConnectionsChange 38 | ? onConnectionsChange.apply(this, arguments) 39 | : undefined 40 | // TODO: remove all widgets on disconnect once computed 41 | shared.dynamic_connection(this, index, connected, 'anything_', '*') 42 | 43 | //- infer type 44 | if (link_info) { 45 | const fromNode = this.graph._nodes.find( 46 | (otherNode) => otherNode.id == link_info.origin_id 47 | ) 48 | const type = fromNode.outputs[link_info.origin_slot].type 49 | this.inputs[index].type = type 50 | // this.inputs[index].label = type.toLowerCase() 51 | } 52 | //- restore dynamic input 53 | if (!connected) { 54 | this.inputs[index].type = '*' 55 | this.inputs[index].label = `anything_${index + 1}` 56 | } 57 | } 58 | 59 | const onExecuted = nodeType.prototype.onExecuted 60 | nodeType.prototype.onExecuted = function (message) { 61 | onExecuted?.apply(this, arguments) 62 | 63 | const prefix = 'anything_' 64 | 65 | if (this.widgets) { 66 | // const pos = this.widgets.findIndex((w) => w.name === "anything_1"); 67 | // if (pos !== -1) { 68 | for (let i = 0; i < this.widgets.length; i++) { 69 | this.widgets[i].onRemoved?.() 70 | } 71 | this.widgets.length = 0 72 | } 73 | let widgetI = 1 74 | if (message.text) { 75 | for (const txt of message.text) { 76 | const w = this.addCustomWidget( 77 | DZWidgets.DEBUG_STRING(`${prefix}_${widgetI}`, escapeHtml(txt)) 78 | ) 79 | w.parent = this 80 | widgetI++ 81 | } 82 | } 83 | if (message.b64_images) { 84 | for (const img of message.b64_images) { 85 | const w = this.addCustomWidget( 86 | DZWidgets.DEBUG_IMG(`${prefix}_${widgetI}`, img) 87 | ) 88 | w.parent = this 89 | widgetI++ 90 | } 91 | // this.onResize?.(this.size); 92 | // this.resize?.(this.size) 93 | } 94 | 95 | this.setSize(this.computeSize()) 96 | 97 | this.onRemoved = function () { 98 | // When removing this node we need to remove the input from the DOM 99 | for (let y in this.widgets) { 100 | if (this.widgets[y].canvas) { 101 | this.widgets[y].canvas.remove() 102 | } 103 | this.widgets[y].onRemoved?.() 104 | } 105 | } 106 | } 107 | } 108 | }, 109 | }) 110 | -------------------------------------------------------------------------------- /mtb/dz_mtb_widgets.js: -------------------------------------------------------------------------------- 1 | /** 2 | * File: DZ_widgets.js 3 | * Project: comfy_DZ 4 | * Author: Mel Massadian 5 | * 6 | * Copyright (c) 2023 Mel Massadian 7 | * 8 | */ 9 | 10 | import { app } from '../../scripts/app.js' 11 | import { api } from '../../scripts/api.js' 12 | 13 | import parseCss from './dz_parse-css.js' 14 | import * as shared from './dz_comfy_shared.js' 15 | import { log } from './dz_comfy_shared.js' 16 | 17 | const newTypes = [, /*'BOOL'*/ 'COLOR', 'BBOX'] 18 | 19 | const withFont = (ctx, font, cb) => { 20 | const oldFont = ctx.font 21 | ctx.font = font 22 | cb() 23 | ctx.font = oldFont 24 | } 25 | 26 | const calculateTextDimensions = (ctx, value, width, fontSize = 16) => { 27 | const words = value.split(' ') 28 | const lines = [] 29 | let currentLine = '' 30 | for (const word of words) { 31 | const testLine = currentLine.length === 0 ? word : `${currentLine} ${word}` 32 | const testWidth = ctx.measureText(testLine).width 33 | if (testWidth > width) { 34 | lines.push(currentLine) 35 | currentLine = word 36 | } else { 37 | currentLine = testLine 38 | } 39 | } 40 | if (lines.length === 0) lines.push(value) 41 | const textHeight = (lines.length + 1) * fontSize 42 | const maxLineWidth = lines.reduce( 43 | (maxWidth, line) => Math.max(maxWidth, ctx.measureText(line).width), 44 | 0 45 | ) 46 | return { textHeight, maxLineWidth } 47 | } 48 | 49 | export const DZWidgets = { 50 | BBOX: (key, val) => { 51 | /** @type {import("./types/litegraph").IWidget} */ 52 | const widget = { 53 | name: key, 54 | type: 'BBOX', 55 | // options: val, 56 | y: 0, 57 | value: val?.default || [0, 0, 0, 0], 58 | options: {}, 59 | 60 | draw: function (ctx, node, widget_width, widgetY, height) { 61 | const hide = this.type !== 'BBOX' && app.canvas.ds.scale > 0.5 62 | 63 | const show_text = true 64 | const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR 65 | const background_color = LiteGraph.WIDGET_BGCOLOR 66 | const text_color = LiteGraph.WIDGET_TEXT_COLOR 67 | const secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR 68 | const H = LiteGraph.NODE_WIDGET_HEIGHT 69 | 70 | let margin = 15 71 | let numWidgets = 4 // Number of stacked widgets 72 | 73 | if (hide) return 74 | 75 | for (let i = 0; i < numWidgets; i++) { 76 | let currentY = widgetY + i * (H + margin) // Adjust Y position for each widget 77 | 78 | ctx.textAlign = 'left' 79 | ctx.strokeStyle = outline_color 80 | ctx.fillStyle = background_color 81 | ctx.beginPath() 82 | if (show_text) 83 | ctx.roundRect(margin, currentY, widget_width - margin * 2, H, [ 84 | H * 0.5, 85 | ]) 86 | else ctx.rect(margin, currentY, widget_width - margin * 2, H) 87 | ctx.fill() 88 | if (show_text) { 89 | if (!this.disabled) ctx.stroke() 90 | ctx.fillStyle = text_color 91 | if (!this.disabled) { 92 | ctx.beginPath() 93 | ctx.moveTo(margin + 16, currentY + 5) 94 | ctx.lineTo(margin + 6, currentY + H * 0.5) 95 | ctx.lineTo(margin + 16, currentY + H - 5) 96 | ctx.fill() 97 | ctx.beginPath() 98 | ctx.moveTo(widget_width - margin - 16, currentY + 5) 99 | ctx.lineTo(widget_width - margin - 6, currentY + H * 0.5) 100 | ctx.lineTo(widget_width - margin - 16, currentY + H - 5) 101 | ctx.fill() 102 | } 103 | ctx.fillStyle = secondary_text_color 104 | ctx.fillText( 105 | this.label || this.name, 106 | margin * 2 + 5, 107 | currentY + H * 0.7 108 | ) 109 | ctx.fillStyle = text_color 110 | ctx.textAlign = 'right' 111 | 112 | ctx.fillText( 113 | Number(this.value).toFixed( 114 | this.options?.precision !== undefined 115 | ? this.options.precision 116 | : 3 117 | ), 118 | widget_width - margin * 2 - 20, 119 | currentY + H * 0.7 120 | ) 121 | } 122 | } 123 | }, 124 | mouse: function (event, pos, node) { 125 | let old_value = this.value 126 | let x = pos[0] - node.pos[0] 127 | let y = pos[1] - node.pos[1] 128 | let width = node.size[0] 129 | let H = LiteGraph.NODE_WIDGET_HEIGHT 130 | let margin = 5 131 | let numWidgets = 4 // Number of stacked widgets 132 | 133 | for (let i = 0; i < numWidgets; i++) { 134 | let currentY = y + i * (H + margin) // Adjust Y position for each widget 135 | 136 | if ( 137 | event.type == LiteGraph.pointerevents_method + 'move' && 138 | this.type == 'BBOX' 139 | ) { 140 | if (event.deltaX) 141 | this.value += event.deltaX * 0.1 * (this.options?.step || 1) 142 | if (this.options.min != null && this.value < this.options.min) { 143 | this.value = this.options.min 144 | } 145 | if (this.options.max != null && this.value > this.options.max) { 146 | this.value = this.options.max 147 | } 148 | } else if (event.type == LiteGraph.pointerevents_method + 'down') { 149 | let values = this.options?.values 150 | if (values && values.constructor === Function) { 151 | values = this.options.values(w, node) 152 | } 153 | let values_list = null 154 | 155 | let delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0 156 | if (this.type == 'BBOX') { 157 | this.value += delta * 0.1 * (this.options.step || 1) 158 | if (this.options.min != null && this.value < this.options.min) { 159 | this.value = this.options.min 160 | } 161 | if (this.options.max != null && this.value > this.options.max) { 162 | this.value = this.options.max 163 | } 164 | } else if (delta) { 165 | //clicked in arrow, used for combos 166 | let index = -1 167 | this.last_mouseclick = 0 //avoids dobl click event 168 | if (values.constructor === Object) 169 | index = values_list.indexOf(String(this.value)) + delta 170 | else index = values_list.indexOf(this.value) + delta 171 | if (index >= values_list.length) { 172 | index = values_list.length - 1 173 | } 174 | if (index < 0) { 175 | index = 0 176 | } 177 | if (values.constructor === Array) this.value = values[index] 178 | else this.value = index 179 | } 180 | } //end mousedown 181 | else if ( 182 | event.type == LiteGraph.pointerevents_method + 'up' && 183 | this.type == 'BBOX' 184 | ) { 185 | let delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0 186 | if (event.click_time < 200 && delta == 0) { 187 | this.prompt( 188 | 'Value', 189 | this.value, 190 | function (v) { 191 | // check if v is a valid equation or a number 192 | if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) { 193 | try { 194 | //solve the equation if possible 195 | v = eval(v) 196 | } catch (e) {} 197 | } 198 | this.value = Number(v) 199 | shared.inner_value_change(this, this.value, event) 200 | }.bind(w), 201 | event 202 | ) 203 | } 204 | } 205 | 206 | if (old_value != this.value) 207 | setTimeout( 208 | function () { 209 | shared.inner_value_change(this, this.value, event) 210 | }.bind(this), 211 | 20 212 | ) 213 | 214 | app.canvas.setDirty(true) 215 | } 216 | }, 217 | computeSize: function (width) { 218 | return [width, LiteGraph.NODE_WIDGET_HEIGHT * 4] 219 | }, 220 | // onDrawBackground: function (ctx) { 221 | // if (!this.flags.collapsed) return; 222 | // this.inputEl.style.display = "block"; 223 | // this.inputEl.style.top = this.graphcanvas.offsetTop + this.pos[1] + "px"; 224 | // this.inputEl.style.left = this.graphcanvas.offsetLeft + this.pos[0] + "px"; 225 | // }, 226 | // onInputChange: function (e) { 227 | // const property = e.target.dataset.property; 228 | // const bbox = this.getInputData(0); 229 | // if (!bbox) return; 230 | // bbox[property] = parseFloat(e.target.value); 231 | // this.setOutputData(0, bbox); 232 | // } 233 | } 234 | 235 | widget.desc = 'Represents a Bounding Box with x, y, width, and height.' 236 | return widget 237 | }, 238 | 239 | COLOR: (key, val, compute = false) => { 240 | /** @type {import("/types/litegraph").IWidget} */ 241 | const widget = {} 242 | widget.y = 0 243 | widget.name = key 244 | widget.type = 'COLOR' 245 | widget.options = { default: '#ff0000' } 246 | widget.value = val || '#ff0000' 247 | widget.draw = function (ctx, node, widgetWidth, widgetY, height) { 248 | const hide = this.type !== 'COLOR' && app.canvas.ds.scale > 0.5 249 | if (hide) { 250 | return 251 | } 252 | const border = 3 253 | ctx.fillStyle = '#000' 254 | ctx.fillRect(0, widgetY, widgetWidth, height) 255 | ctx.fillStyle = this.value 256 | ctx.fillRect( 257 | border, 258 | widgetY + border, 259 | widgetWidth - border * 2, 260 | height - border * 2 261 | ) 262 | const color = parseCss(this.value.default || this.value) 263 | if (!color) { 264 | return 265 | } 266 | ctx.fillStyle = shared.isColorBright(color.values, 125) ? '#000' : '#fff' 267 | 268 | ctx.font = '14px Arial' 269 | ctx.textAlign = 'center' 270 | ctx.fillText(this.name, widgetWidth * 0.5, widgetY + 14) 271 | } 272 | widget.mouse = function (e, pos, node) { 273 | if (e.type === 'pointerdown') { 274 | const widgets = node.widgets.filter((w) => w.type === 'COLOR') 275 | 276 | for (const w of widgets) { 277 | // color picker 278 | const rect = [w.last_y, w.last_y + 32] 279 | if (pos[1] > rect[0] && pos[1] < rect[1]) { 280 | const picker = document.createElement('input') 281 | picker.type = 'color' 282 | picker.value = this.value 283 | 284 | picker.style.position = 'absolute' 285 | picker.style.left = '999999px' //(window.innerWidth / 2) + "px"; 286 | picker.style.top = '999999px' //(window.innerHeight / 2) + "px"; 287 | 288 | document.body.appendChild(picker) 289 | 290 | picker.addEventListener('change', () => { 291 | this.value = picker.value 292 | node.graph._version++ 293 | node.setDirtyCanvas(true, true) 294 | picker.remove() 295 | }) 296 | 297 | picker.click() 298 | } 299 | } 300 | } 301 | } 302 | widget.computeSize = function (width) { 303 | return [width, 32] 304 | } 305 | 306 | return widget 307 | }, 308 | 309 | DEBUG_IMG: (name, val) => { 310 | const w = { 311 | name, 312 | type: 'image', 313 | value: val, 314 | draw: function (ctx, node, widgetWidth, widgetY, height) { 315 | const [cw, ch] = this.computeSize(widgetWidth) 316 | shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch) 317 | }, 318 | computeSize: function (width) { 319 | const ratio = this.inputRatio || 1 320 | if (width) { 321 | return [width, width / ratio + 4] 322 | } 323 | return [128, 128] 324 | }, 325 | onRemoved: function () { 326 | if (this.inputEl) { 327 | this.inputEl.remove() 328 | } 329 | }, 330 | } 331 | 332 | w.inputEl = document.createElement('img') 333 | w.inputEl.src = w.value 334 | w.inputEl.onload = function () { 335 | w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight 336 | } 337 | document.body.appendChild(w.inputEl) 338 | return w 339 | }, 340 | DEBUG_STRING: (name, val) => { 341 | const fontSize = 16 342 | const w = { 343 | name, 344 | type: 'debug_text', 345 | 346 | draw: function (ctx, node, widgetWidth, widgetY, height) { 347 | // const [cw, ch] = this.computeSize(widgetWidth) 348 | shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, height) 349 | }, 350 | computeSize(width) { 351 | if (!this.value) { 352 | return [32, 32] 353 | } 354 | if (!width) { 355 | console.debug(`No width ${this.parent.size}`) 356 | } 357 | let dimensions 358 | withFont(app.ctx, `${fontSize}px monospace`, () => { 359 | dimensions = calculateTextDimensions(app.ctx, this.value, width) 360 | }) 361 | const widgetWidth = Math.max( 362 | width || this.width || 32, 363 | dimensions.maxLineWidth 364 | ) 365 | const widgetHeight = dimensions.textHeight * 1.5 366 | return [widgetWidth, widgetHeight] 367 | }, 368 | onRemoved: function () { 369 | if (this.inputEl) { 370 | this.inputEl.remove() 371 | } 372 | }, 373 | get value() { 374 | return this.inputEl.innerHTML 375 | }, 376 | set value(val) { 377 | this.inputEl.innerHTML = val 378 | this.parent?.setSize?.(this.parent?.computeSize()) 379 | }, 380 | } 381 | 382 | w.inputEl = document.createElement('p') 383 | w.inputEl.style = ` 384 | text-align: center; 385 | font-size: ${fontSize}px; 386 | color: var(--input-text); 387 | line-height: 0; 388 | font-family: monospace; 389 | ` 390 | w.value = val 391 | document.body.appendChild(w.inputEl) 392 | 393 | return w 394 | }, 395 | } 396 | 397 | /** 398 | * @returns {import("./types/comfy").ComfyExtension} extension 399 | */ 400 | const DZ_widgets = { 401 | name: 'DZ.widgets', 402 | 403 | init: async () => { 404 | log('Registering DZ.widgets') 405 | try { 406 | const res = await api.fetchApi('/DZ/debug') 407 | const msg = await res.json() 408 | if (!window.DZ) { 409 | window.DZ = {} 410 | } 411 | window.DZ.DEBUG = msg.enabled 412 | } catch (e) { 413 | console.error('Error:', error) 414 | } 415 | }, 416 | 417 | setup: () => { 418 | app.ui.settings.addSetting({ 419 | id: 'DZ.Debug.enabled', 420 | name: '[DZ] Enable Debug (py and js)', 421 | type: 'boolean', 422 | defaultValue: false, 423 | 424 | tooltip: 425 | 'This will enable debug messages in the console and in the python console respectively', 426 | attrs: { 427 | style: { 428 | fontFamily: 'monospace', 429 | }, 430 | }, 431 | async onChange(value) { 432 | if (value) { 433 | console.log('Enabled DEBUG mode') 434 | } 435 | if (!window.DZ) { 436 | window.DZ = {} 437 | } 438 | window.DZ.DEBUG = value 439 | await api 440 | .fetchApi('/DZ/debug', { 441 | method: 'POST', 442 | body: JSON.stringify({ 443 | enabled: value, 444 | }), 445 | }) 446 | .then((response) => {}) 447 | .catch((error) => { 448 | console.error('Error:', error) 449 | }) 450 | }, 451 | }) 452 | }, 453 | 454 | getCustomWidgets: function () { 455 | return { 456 | BOOL: (node, inputName, inputData, app) => { 457 | console.debug('Registering bool') 458 | 459 | return { 460 | widget: node.addCustomWidget( 461 | DZWidgets.BOOL(inputName, inputData[1]?.default || false) 462 | ), 463 | minWidth: 150, 464 | minHeight: 30, 465 | } 466 | }, 467 | 468 | COLOR: (node, inputName, inputData, app) => { 469 | console.debug('Registering color') 470 | return { 471 | widget: node.addCustomWidget( 472 | DZWidgets.COLOR(inputName, inputData[1]?.default || '#ff0000') 473 | ), 474 | minWidth: 150, 475 | minHeight: 30, 476 | } 477 | }, 478 | // BBOX: (node, inputName, inputData, app) => { 479 | // console.debug("Registering bbox") 480 | // return { 481 | // widget: node.addCustomWidget(DZWidgets.BBOX(inputName, inputData[1]?.default || [0, 0, 0, 0])), 482 | // minWidth: 150, 483 | // minHeight: 30, 484 | // } 485 | 486 | // } 487 | } 488 | }, 489 | /** 490 | * @param {import("./types/comfy").NodeType} nodeType 491 | * @param {import("./types/comfy").NodeDef} nodeData 492 | * @param {import("./types/comfy").App} app 493 | */ 494 | async beforeRegisterNodeDef(nodeType, nodeData, app) { 495 | // const rinputs = nodeData.input?.required 496 | 497 | let has_custom = false 498 | if (nodeData.input && nodeData.input.required) { 499 | for (const i of Object.keys(nodeData.input.required)) { 500 | const input_type = nodeData.input.required[i][0] 501 | 502 | if (newTypes.includes(input_type)) { 503 | has_custom = true 504 | break 505 | } 506 | } 507 | } 508 | if (has_custom) { 509 | //- Add widgets on node creation 510 | const onNodeCreated = nodeType.prototype.onNodeCreated 511 | nodeType.prototype.onNodeCreated = function () { 512 | const r = onNodeCreated 513 | ? onNodeCreated.apply(this, arguments) 514 | : undefined 515 | this.serialize_widgets = true 516 | this.setSize?.(this.computeSize()) 517 | 518 | this.onRemoved = function () { 519 | // When removing this node we need to remove the input from the DOM 520 | shared.cleanupNode(this) 521 | } 522 | return r 523 | } 524 | 525 | //- Extra menus 526 | const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions 527 | nodeType.prototype.getExtraMenuOptions = function (_, options) { 528 | const r = origGetExtraMenuOptions 529 | ? origGetExtraMenuOptions.apply(this, arguments) 530 | : undefined 531 | if (this.widgets) { 532 | let toInput = [] 533 | let toWidget = [] 534 | for (const w of this.widgets) { 535 | if (w.type === shared.CONVERTED_TYPE) { 536 | //- This is already handled by widgetinputs.js 537 | // toWidget.push({ 538 | // content: `Convert ${w.name} to widget`, 539 | // callback: () => shared.convertToWidget(this, w), 540 | // }); 541 | } else if (newTypes.includes(w.type)) { 542 | const config = nodeData?.input?.required[w.name] || 543 | nodeData?.input?.optional?.[w.name] || [w.type, w.options || {}] 544 | 545 | toInput.push({ 546 | content: `Convert ${w.name} to input`, 547 | callback: () => shared.convertToInput(this, w, config), 548 | }) 549 | } 550 | } 551 | if (toInput.length) { 552 | options.push(...toInput, null) 553 | } 554 | 555 | if (toWidget.length) { 556 | options.push(...toWidget, null) 557 | } 558 | } 559 | 560 | return r 561 | } 562 | } 563 | 564 | //- Extending Python Nodes 565 | switch (nodeData.name) { 566 | case 'Psd Save (DZ)': { 567 | const onConnectionsChange = nodeType.prototype.onConnectionsChange 568 | nodeType.prototype.onConnectionsChange = function ( 569 | type, 570 | index, 571 | connected, 572 | link_info 573 | ) { 574 | const r = onConnectionsChange 575 | ? onConnectionsChange.apply(this, arguments) 576 | : undefined 577 | shared.dynamic_connection(this, index, connected) 578 | return r 579 | } 580 | break 581 | } 582 | //TODO: remove this non sense 583 | case 'Get Batch From History (DZ)': { 584 | const onNodeCreated = nodeType.prototype.onNodeCreated 585 | nodeType.prototype.onNodeCreated = function () { 586 | const r = onNodeCreated 587 | ? onNodeCreated.apply(this, arguments) 588 | : undefined 589 | const internal_count = this.widgets.find( 590 | (w) => w.name === 'internal_count' 591 | ) 592 | shared.hideWidgetForGood(this, internal_count) 593 | internal_count.afterQueued = function () { 594 | this.value++ 595 | } 596 | 597 | return r 598 | } 599 | 600 | const onExecuted = nodeType.prototype.onExecuted 601 | nodeType.prototype.onExecuted = function (message) { 602 | const r = onExecuted ? onExecuted.apply(this, message) : undefined 603 | return r 604 | } 605 | 606 | break 607 | } 608 | case 'Save Gif (DZ)': 609 | case 'Save Animated Image (DZ)': { 610 | const onExecuted = nodeType.prototype.onExecuted 611 | nodeType.prototype.onExecuted = function (message) { 612 | const prefix = 'anything_' 613 | const r = onExecuted ? onExecuted.apply(this, message) : undefined 614 | 615 | if (this.widgets) { 616 | const pos = this.widgets.findIndex((w) => w.name === `${prefix}_0`) 617 | if (pos !== -1) { 618 | for (let i = pos; i < this.widgets.length; i++) { 619 | this.widgets[i].onRemoved?.() 620 | } 621 | this.widgets.length = pos 622 | } 623 | 624 | let imgURLs = [] 625 | if (message) { 626 | if (message.gif) { 627 | imgURLs = imgURLs.concat( 628 | message.gif.map((params) => { 629 | return api.apiURL( 630 | '/view?' + new URLSearchParams(params).toString() 631 | ) 632 | }) 633 | ) 634 | } 635 | if (message.apng) { 636 | imgURLs = imgURLs.concat( 637 | message.apng.map((params) => { 638 | return api.apiURL( 639 | '/view?' + new URLSearchParams(params).toString() 640 | ) 641 | }) 642 | ) 643 | } 644 | let i = 0 645 | for (const img of imgURLs) { 646 | const w = this.addCustomWidget( 647 | DZWidgets.DEBUG_IMG(`${prefix}_${i}`, img) 648 | ) 649 | w.parent = this 650 | i++ 651 | } 652 | } 653 | const onRemoved = this.onRemoved 654 | this.onRemoved = () => { 655 | shared.cleanupNode(this) 656 | return onRemoved?.() 657 | } 658 | } 659 | this.setSize?.(this.computeSize()) 660 | return r 661 | } 662 | 663 | break 664 | } 665 | case 'Animation Builder (DZ)': { 666 | const onNodeCreated = nodeType.prototype.onNodeCreated 667 | nodeType.prototype.onNodeCreated = function () { 668 | const r = onNodeCreated 669 | ? onNodeCreated.apply(this, arguments) 670 | : undefined 671 | 672 | this.changeMode(LiteGraph.ALWAYS) 673 | 674 | const raw_iteration = this.widgets.find( 675 | (w) => w.name === 'raw_iteration' 676 | ) 677 | const raw_loop = this.widgets.find((w) => w.name === 'raw_loop') 678 | 679 | const total_frames = this.widgets.find( 680 | (w) => w.name === 'total_frames' 681 | ) 682 | const loop_count = this.widgets.find((w) => w.name === 'loop_count') 683 | 684 | shared.hideWidgetForGood(this, raw_iteration) 685 | shared.hideWidgetForGood(this, raw_loop) 686 | 687 | raw_iteration._value = 0 688 | 689 | const value_preview = this.addCustomWidget( 690 | DZWidgets['DEBUG_STRING']('value_preview', 'Idle') 691 | ) 692 | value_preview.parent = this 693 | 694 | const loop_preview = this.addCustomWidget( 695 | DZWidgets['DEBUG_STRING']('loop_preview', 'Iteration: Idle') 696 | ) 697 | loop_preview.parent = this 698 | 699 | const onReset = () => { 700 | raw_iteration.value = 0 701 | raw_loop.value = 0 702 | 703 | value_preview.value = 'Idle' 704 | loop_preview.value = 'Iteration: Idle' 705 | 706 | app.canvas.setDirty(true) 707 | } 708 | 709 | const reset_button = this.addWidget( 710 | 'button', 711 | `Reset`, 712 | 'reset', 713 | onReset 714 | ) 715 | 716 | const run_button = this.addWidget('button', `Queue`, 'queue', () => { 717 | onReset() // this could maybe be a setting or checkbox 718 | app.queuePrompt(0, total_frames.value * loop_count.value) 719 | window.DZ?.notify?.( 720 | `Started a queue of ${total_frames.value} frames (for ${ 721 | loop_count.value 722 | } loop, so ${total_frames.value * loop_count.value})`, 723 | 5000 724 | ) 725 | }) 726 | 727 | this.onRemoved = () => { 728 | shared.cleanupNode(this) 729 | app.canvas.setDirty(true) 730 | } 731 | 732 | raw_iteration.afterQueued = function () { 733 | this.value++ 734 | raw_loop.value = Math.floor(this.value / total_frames.value) 735 | 736 | value_preview.value = `frame: ${ 737 | raw_iteration.value % total_frames.value 738 | } / ${total_frames.value - 1}` 739 | 740 | if (raw_loop.value + 1 > loop_count.value) { 741 | loop_preview.value = 'Done 😎!' 742 | } else { 743 | loop_preview.value = `current loop: ${raw_loop.value + 1}/${ 744 | loop_count.value 745 | }` 746 | } 747 | } 748 | 749 | return r 750 | } 751 | 752 | break 753 | } 754 | case 'Text Encore Frames (DZ)': { 755 | const onConnectionsChange = nodeType.prototype.onConnectionsChange 756 | nodeType.prototype.onConnectionsChange = function ( 757 | type, 758 | index, 759 | connected, 760 | link_info 761 | ) { 762 | const r = onConnectionsChange 763 | ? onConnectionsChange.apply(this, arguments) 764 | : undefined 765 | 766 | shared.dynamic_connection(this, index, connected) 767 | return r 768 | } 769 | break 770 | } 771 | case 'Interpolate Clip Sequential (DZ)': { 772 | const onNodeCreated = nodeType.prototype.onNodeCreated 773 | nodeType.prototype.onNodeCreated = function () { 774 | const r = onNodeCreated 775 | ? onNodeCreated.apply(this, arguments) 776 | : undefined 777 | const addReplacement = () => { 778 | const input = this.addInput( 779 | `replacement_${this.widgets.length}`, 780 | 'STRING', 781 | '' 782 | ) 783 | console.log(input) 784 | this.addWidget('STRING', `replacement_${this.widgets.length}`, '') 785 | } 786 | //- add 787 | this.addWidget('button', '+', 'add', function (value, widget, node) { 788 | console.log('Button clicked', value, widget, node) 789 | addReplacement() 790 | }) 791 | //- remove 792 | this.addWidget( 793 | 'button', 794 | '-', 795 | 'remove', 796 | function (value, widget, node) { 797 | console.log(`Button clicked: ${value}`, widget, node) 798 | } 799 | ) 800 | 801 | return r 802 | } 803 | break 804 | } 805 | case 'Styles Loader (DZ)': { 806 | const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions 807 | nodeType.prototype.getExtraMenuOptions = function (_, options) { 808 | const r = origGetExtraMenuOptions 809 | ? origGetExtraMenuOptions.apply(this, arguments) 810 | : undefined 811 | 812 | const getStyle = async (node) => { 813 | try { 814 | const getStyles = await api.fetchApi('/DZ/actions', { 815 | method: 'POST', 816 | body: JSON.stringify({ 817 | name: 'getStyles', 818 | args: 819 | node.widgets && node.widgets[0].value 820 | ? node.widgets[0].value 821 | : '', 822 | }), 823 | }) 824 | 825 | const output = await getStyles.json() 826 | return output?.result 827 | } catch (e) { 828 | console.error(e) 829 | } 830 | } 831 | const extracters = [ 832 | { 833 | content: 'Extract Positive to Text node', 834 | callback: async () => { 835 | const style = await getStyle(this) 836 | if (style && style.length >= 1) { 837 | if (style[0]) { 838 | window.DZ?.notify?.( 839 | `Extracted positive from ${this.widgets[0].value}` 840 | ) 841 | const tn = LiteGraph.createNode('Text box') 842 | app.graph.add(tn) 843 | tn.title = `${this.widgets[0].value} (Positive)` 844 | tn.widgets[0].value = style[0] 845 | } else { 846 | window.DZ?.notify?.( 847 | `No positive to extract for ${this.widgets[0].value}` 848 | ) 849 | } 850 | } 851 | }, 852 | }, 853 | { 854 | content: 'Extract Negative to Text node', 855 | callback: async () => { 856 | const style = await getStyle(this) 857 | if (style && style.length >= 2) { 858 | if (style[1]) { 859 | window.DZ?.notify?.( 860 | `Extracted negative from ${this.widgets[0].value}` 861 | ) 862 | const tn = LiteGraph.createNode('Text box') 863 | app.graph.add(tn) 864 | tn.title = `${this.widgets[0].value} (Negative)` 865 | tn.widgets[0].value = style[1] 866 | } else { 867 | window.DZ.notify( 868 | `No negative to extract for ${this.widgets[0].value}` 869 | ) 870 | } 871 | } 872 | }, 873 | }, 874 | ] 875 | options.push(...extracters) 876 | } 877 | 878 | break 879 | } 880 | case 'Save Tensors (DZ)': { 881 | const onDrawBackground = nodeType.prototype.onDrawBackground 882 | nodeType.prototype.onDrawBackground = function (ctx, canvas) { 883 | const r = onDrawBackground 884 | ? onDrawBackground.apply(this, arguments) 885 | : undefined 886 | // // draw a circle on the top right of the node, with text inside 887 | // ctx.fillStyle = "#fff"; 888 | // ctx.beginPath(); 889 | // ctx.arc(this.size[0] - this.node_width * 0.5, this.size[1] - this.node_height * 0.5, this.node_width * 0.5, 0, Math.PI * 2); 890 | // ctx.fill(); 891 | 892 | // ctx.fillStyle = "#000"; 893 | // ctx.textAlign = "center"; 894 | // ctx.font = "bold 12px Arial"; 895 | // ctx.fillText("Save Tensors", this.size[0] - this.node_width * 0.5, this.size[1] - this.node_height * 0.5); 896 | 897 | return r 898 | } 899 | break 900 | } 901 | default: { 902 | break 903 | } 904 | } 905 | }, 906 | } 907 | 908 | app.registerExtension(DZ_widgets) 909 | -------------------------------------------------------------------------------- /mtb/dz_parse-css.js: -------------------------------------------------------------------------------- 1 | 2 | // #region patterns 3 | const float = '-?\\d*(?:\\.\\d+)'; 4 | export const number = `(${float}?)`; 5 | export const percentage = `(${float}?%)`; 6 | export const numberOrPercentage = `(${float}?%?)`; 7 | const clamp = (num, min, max) => Math.min(Math.max(min, num), max); 8 | 9 | const hexCharacters = 'a-f\\d'; 10 | const match3or4Hex = `#?[${hexCharacters}]{3}[${hexCharacters}]?`; 11 | const match6or8Hex = `#?[${hexCharacters}]{6}([${hexCharacters}]{2})?`; 12 | const nonHexChars = new RegExp(`[^#${hexCharacters}]`, 'gi'); 13 | const validHexSize = new RegExp(`^${match3or4Hex}$|^${match6or8Hex}$`, 'i'); 14 | 15 | 16 | export const hex_pattern = new RegExp(/^#([a-f0-9]{3,4}|[a-f0-9]{4}(?:[a-f0-9]{2}){1,2})\b$/, "i"); 17 | 18 | export const hsl3_pattern = new RegExp(`^ 19 | hsla?\\( 20 | \\s*(-?\\d*(?:\\.\\d+)?(?:deg|rad|turn)?)\\s*, 21 | \\s*${percentage}\\s*, 22 | \\s*${percentage}\\s* 23 | (?:,\\s*${numberOrPercentage}\\s*)? 24 | \\) 25 | $ 26 | `.replace(/\n|\s/g, '')) 27 | 28 | export const hsl4_pattern = new RegExp(`^ 29 | hsla?\\( 30 | \\s*(-?\\d*(?:\\.\\d+)?(?:deg|rad|turn)?)\\s* 31 | \\s+${percentage} 32 | \\s+${percentage} 33 | \\s*(?:\\s*\\/\\s*${numberOrPercentage}\\s*)? 34 | \\) 35 | $ 36 | `.replace(/\n|\s/g, '')) 37 | 38 | export const rgb3_pattern = new RegExp(`^ 39 | rgba?\\( 40 | \\s*${number}\\s*, 41 | \\s*${number}\\s*, 42 | \\s*${number}\\s* 43 | (?:,\\s*${numberOrPercentage}\\s*)? 44 | \\) 45 | $ 46 | `.replace(/\n|\s/g, '')) 47 | 48 | export const rgb4_pattern = new RegExp(`^ 49 | rgba?\\( 50 | \\s*${number} 51 | \\s+${number} 52 | \\s+${number} 53 | \\s*(?:\\s*\\/\\s*${numberOrPercentage}\\s*)? 54 | \\) 55 | $ 56 | `.replace(/\n|\s/g, '')); 57 | 58 | export const transparent_pattern = new RegExp(/^transparent$/, 'i'); 59 | // #endregion 60 | 61 | 62 | // #region utils 63 | 64 | 65 | /* 500 => 255, -10 => 0, 128 => 128 */ 66 | const parseRGB = (num) => { 67 | let n = num; 68 | if (typeof n !== 'number') { 69 | n = n.endsWith('%') ? (parseFloat(n) * 255) / 100 : parseFloat(n); 70 | } 71 | return clamp(Math.round(n), 0, 255); 72 | }; 73 | 74 | /* 200 => 100, -100 => 0, 50 => 50 */ 75 | const parsePercentage = (percentage) => clamp(parseFloat(percentage), 0, 100); 76 | 77 | /* '50%' => 5.0, 200 => 1, -10 => 0 */ 78 | function parseAlpha(alpha) { 79 | let a = alpha; 80 | if (typeof a !== 'number') { 81 | a = a.endsWith('%') ? parseFloat(a) / 100 : parseFloat(a); 82 | } 83 | return clamp(a, 0, 1); 84 | } 85 | 86 | export function getHEX(hex) { 87 | const [r, g, b, a] = hex2Rgb(hex, { format: 'array' }); 88 | return getRGB([null, ...[r, g, b, a]]); 89 | } 90 | 91 | export function getHSL([, h, s, l, a = 1]) { 92 | let hh = h; 93 | if (hh.endsWith('turn')) { 94 | hh = (parseFloat(hh) * 360) / 1; 95 | } else if (hh.endsWith('rad')) { 96 | hh = Math.round((parseFloat(hh) * 180) / Math.PI); 97 | } else { 98 | hh = parseFloat(hh); 99 | } 100 | return { 101 | type: 'hsl', 102 | values: [hh, parsePercentage(s), parsePercentage(l)], 103 | alpha: parseAlpha(a === null ? 1 : a) 104 | }; 105 | } 106 | 107 | export function getRGB([, r, g, b, a = 1]) { 108 | return { 109 | type: 'rgb', 110 | values: [r, g, b].map(parseRGB), 111 | alpha: parseAlpha(a === null ? 1 : a) 112 | }; 113 | } 114 | export function hex2Rgb(hex, options = {}) { 115 | if (typeof hex !== 'string' || nonHexChars.test(hex) || !validHexSize.test(hex)) { 116 | throw new TypeError('Expected a valid hex string'); 117 | } 118 | 119 | hex = hex.replace(/^#/, ''); 120 | let alphaFromHex = 1; 121 | 122 | if (hex.length === 8) { 123 | alphaFromHex = Number.parseInt(hex.slice(6, 8), 16) / 255; 124 | hex = hex.slice(0, 6); 125 | } 126 | 127 | if (hex.length === 4) { 128 | alphaFromHex = Number.parseInt(hex.slice(3, 4).repeat(2), 16) / 255; 129 | hex = hex.slice(0, 3); 130 | } 131 | 132 | if (hex.length === 3) { 133 | hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; 134 | } 135 | 136 | const number = Number.parseInt(hex, 16); 137 | const red = number >> 16; 138 | const green = (number >> 8) & 255; 139 | const blue = number & 255; 140 | const alpha = typeof options.alpha === 'number' ? options.alpha : alphaFromHex; 141 | 142 | if (options.format === 'array') { 143 | return [red, green, blue, alpha]; 144 | } 145 | 146 | if (options.format === 'css') { 147 | const alphaString = alpha === 1 ? '' : ` / ${Number((alpha * 100).toFixed(2))}%`; 148 | return `rgb(${red} ${green} ${blue}${alphaString})`; 149 | } 150 | 151 | return {red, green, blue, alpha}; 152 | } 153 | // #endregion 154 | 155 | 156 | // #region colorNames 157 | export const colorName = { 158 | aliceblue: [240, 248, 255], 159 | antiquewhite: [250, 235, 215], 160 | aqua: [0, 255, 255], 161 | aquamarine: [127, 255, 212], 162 | azure: [240, 255, 255], 163 | beige: [245, 245, 220], 164 | bisque: [255, 228, 196], 165 | black: [0, 0, 0], 166 | blanchedalmond: [255, 235, 205], 167 | blue: [0, 0, 255], 168 | blueviolet: [138, 43, 226], 169 | brown: [165, 42, 42], 170 | burlywood: [222, 184, 135], 171 | cadetblue: [95, 158, 160], 172 | chartreuse: [127, 255, 0], 173 | chocolate: [210, 105, 30], 174 | coral: [255, 127, 80], 175 | cornflowerblue: [100, 149, 237], 176 | cornsilk: [255, 248, 220], 177 | crimson: [220, 20, 60], 178 | cyan: [0, 255, 255], 179 | darkblue: [0, 0, 139], 180 | darkcyan: [0, 139, 139], 181 | darkgoldenrod: [184, 134, 11], 182 | darkgray: [169, 169, 169], 183 | darkgreen: [0, 100, 0], 184 | darkgrey: [169, 169, 169], 185 | darkkhaki: [189, 183, 107], 186 | darkmagenta: [139, 0, 139], 187 | darkolivegreen: [85, 107, 47], 188 | darkorange: [255, 140, 0], 189 | darkorchid: [153, 50, 204], 190 | darkred: [139, 0, 0], 191 | darksalmon: [233, 150, 122], 192 | darkseagreen: [143, 188, 143], 193 | darkslateblue: [72, 61, 139], 194 | darkslategray: [47, 79, 79], 195 | darkslategrey: [47, 79, 79], 196 | darkturquoise: [0, 206, 209], 197 | darkviolet: [148, 0, 211], 198 | deeppink: [255, 20, 147], 199 | deepskyblue: [0, 191, 255], 200 | dimgray: [105, 105, 105], 201 | dimgrey: [105, 105, 105], 202 | dodgerblue: [30, 144, 255], 203 | firebrick: [178, 34, 34], 204 | floralwhite: [255, 250, 240], 205 | forestgreen: [34, 139, 34], 206 | fuchsia: [255, 0, 255], 207 | gainsboro: [220, 220, 220], 208 | ghostwhite: [248, 248, 255], 209 | gold: [255, 215, 0], 210 | goldenrod: [218, 165, 32], 211 | gray: [128, 128, 128], 212 | green: [0, 128, 0], 213 | greenyellow: [173, 255, 47], 214 | grey: [128, 128, 128], 215 | honeydew: [240, 255, 240], 216 | hotpink: [255, 105, 180], 217 | indianred: [205, 92, 92], 218 | indigo: [75, 0, 130], 219 | ivory: [255, 255, 240], 220 | khaki: [240, 230, 140], 221 | lavender: [230, 230, 250], 222 | lavenderblush: [255, 240, 245], 223 | lawngreen: [124, 252, 0], 224 | lemonchiffon: [255, 250, 205], 225 | lightblue: [173, 216, 230], 226 | lightcoral: [240, 128, 128], 227 | lightcyan: [224, 255, 255], 228 | lightgoldenrodyellow: [250, 250, 210], 229 | lightgray: [211, 211, 211], 230 | lightgreen: [144, 238, 144], 231 | lightgrey: [211, 211, 211], 232 | lightpink: [255, 182, 193], 233 | lightsalmon: [255, 160, 122], 234 | lightseagreen: [32, 178, 170], 235 | lightskyblue: [135, 206, 250], 236 | lightslategray: [119, 136, 153], 237 | lightslategrey: [119, 136, 153], 238 | lightsteelblue: [176, 196, 222], 239 | lightyellow: [255, 255, 224], 240 | lime: [0, 255, 0], 241 | limegreen: [50, 205, 50], 242 | linen: [250, 240, 230], 243 | magenta: [255, 0, 255], 244 | maroon: [128, 0, 0], 245 | mediumaquamarine: [102, 205, 170], 246 | mediumblue: [0, 0, 205], 247 | mediumorchid: [186, 85, 211], 248 | mediumpurple: [147, 112, 219], 249 | mediumseagreen: [60, 179, 113], 250 | mediumslateblue: [123, 104, 238], 251 | mediumspringgreen: [0, 250, 154], 252 | mediumturquoise: [72, 209, 204], 253 | mediumvioletred: [199, 21, 133], 254 | midnightblue: [25, 25, 112], 255 | mintcream: [245, 255, 250], 256 | mistyrose: [255, 228, 225], 257 | moccasin: [255, 228, 181], 258 | navajowhite: [255, 222, 173], 259 | navy: [0, 0, 128], 260 | oldlace: [253, 245, 230], 261 | olive: [128, 128, 0], 262 | olivedrab: [107, 142, 35], 263 | orange: [255, 165, 0], 264 | orangered: [255, 69, 0], 265 | orchid: [218, 112, 214], 266 | palegoldenrod: [238, 232, 170], 267 | palegreen: [152, 251, 152], 268 | paleturquoise: [175, 238, 238], 269 | palevioletred: [219, 112, 147], 270 | papayawhip: [255, 239, 213], 271 | peachpuff: [255, 218, 185], 272 | peru: [205, 133, 63], 273 | pink: [255, 192, 203], 274 | plum: [221, 160, 221], 275 | powderblue: [176, 224, 230], 276 | purple: [128, 0, 128], 277 | rebeccapurple: [102, 51, 153], 278 | red: [255, 0, 0], 279 | rosybrown: [188, 143, 143], 280 | royalblue: [65, 105, 225], 281 | saddlebrown: [139, 69, 19], 282 | salmon: [250, 128, 114], 283 | sandybrown: [244, 164, 96], 284 | seagreen: [46, 139, 87], 285 | seashell: [255, 245, 238], 286 | sienna: [160, 82, 45], 287 | silver: [192, 192, 192], 288 | skyblue: [135, 206, 235], 289 | slateblue: [106, 90, 205], 290 | slategray: [112, 128, 144], 291 | slategrey: [112, 128, 144], 292 | snow: [255, 250, 250], 293 | springgreen: [0, 255, 127], 294 | steelblue: [70, 130, 180], 295 | tan: [210, 180, 140], 296 | teal: [0, 128, 128], 297 | thistle: [216, 191, 216], 298 | tomato: [255, 99, 71], 299 | turquoise: [64, 224, 208], 300 | violet: [238, 130, 238], 301 | wheat: [245, 222, 179], 302 | white: [255, 255, 255], 303 | whitesmoke: [245, 245, 245], 304 | yellow: [255, 255, 0], 305 | yellowgreen: [154, 205, 50] 306 | } 307 | // #endregion 308 | 309 | 310 | export const parseCSSColor = (str, debug=false) => { 311 | if (typeof str !== 'string') { 312 | console.error(`parseCSSColor: expected a string found ${typeof str}`,str); 313 | return null; 314 | } 315 | 316 | const hex = hex_pattern.exec(str); 317 | if (hex) { 318 | if (debug){ 319 | console.debug('parseCSSColor: hex', hex); 320 | } 321 | return getHEX(hex[0]); 322 | } 323 | 324 | const hsl = hsl4_pattern.exec(str) || hsl3_pattern.exec(str); 325 | if (hsl) { 326 | if (debug){ 327 | console.debug('parseCSSColor: hsl', hsl); 328 | } 329 | return getHSL(hsl); 330 | } 331 | 332 | const rgb = 333 | rgb4_pattern.exec(str) || 334 | rgb3_pattern.exec(str) 335 | if (rgb) { 336 | if (debug){ 337 | console.debug('parseCSSColor: rgb', rgb); 338 | } 339 | return getRGB(rgb); 340 | } 341 | 342 | if (transparent_pattern.exec(str)) { 343 | if (debug){ 344 | console.debug('parseCSSColor: transparent'); 345 | } 346 | return getRGB([null, 0, 0, 0, 0]); 347 | } 348 | 349 | const cn = colorName[str.toLowerCase()]; 350 | if (cn) { 351 | if (debug){ 352 | console.debug('parseCSSColor: colorName', cn); 353 | } 354 | return getRGB([null, cn[0], cn[1], cn[2], 1]); 355 | } 356 | 357 | console.error('parseCSSColor: unknown color', str); 358 | return null; 359 | }; 360 | 361 | export default parseCSSColor; 362 | 363 | -------------------------------------------------------------------------------- /node.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chflame163/ComfyUI_WordCloud/9a20688fae496858f6c0e3c4bf591a3fa5198c9f/node.tar.gz -------------------------------------------------------------------------------- /py/__pycache__/comfy_wordcloud.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chflame163/ComfyUI_WordCloud/9a20688fae496858f6c0e3c4bf591a3fa5198c9f/py/__pycache__/comfy_wordcloud.cpython-311.pyc -------------------------------------------------------------------------------- /py/__pycache__/load_textfile.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chflame163/ComfyUI_WordCloud/9a20688fae496858f6c0e3c4bf591a3fa5198c9f/py/__pycache__/load_textfile.cpython-311.pyc -------------------------------------------------------------------------------- /py/__pycache__/rgb_picker.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chflame163/ComfyUI_WordCloud/9a20688fae496858f6c0e3c4bf591a3fa5198c9f/py/__pycache__/rgb_picker.cpython-311.pyc -------------------------------------------------------------------------------- /py/comfy_wordcloud.py: -------------------------------------------------------------------------------- 1 | import math 2 | import os 3 | import glob 4 | import re 5 | import numpy as np 6 | import torch 7 | import matplotlib.pyplot as plt 8 | from wordcloud import WordCloud, STOPWORDS, ImageColorGenerator 9 | from PIL import Image, ImageChops 10 | import jieba 11 | 12 | def log(message): 13 | name = 'WordCloud' 14 | print(f"# 😺dzNodes: {name} -> {message}") 15 | 16 | COLOR_MAP = ['viridis', 'Accent', 'Blues', 'BrBG', 'BuGn', 'BuPu', 'CMRmap', 'Dark2', 'GnBu', 17 | 'Grays', 'Greens', 'OrRd', 'Oranges', 'PRGn', 'Paired', 'Pastel1', 18 | 'Pastel2', 'PiYG', 'PuBu', 'PuBuGn', 'PuOr', 'PuRd', 'Purples', 'RdBu', 'RdGy', 19 | 'RdPu', 'RdYlBu', 'RdYlGn', 'Reds', 'Set1', 'Set2', 'Set3', 'Spectral', 'Wistia', 20 | 'YlGn', 'YlGnBu', 'YlOrBr', 'YlOrRd', 'afmhot', 'autumn', 'binary', 'bone', 21 | 'brg', 'bwr', 'cividis', 'cool', 'coolwarm', 'copper', 'cubehelix', 'flag', 22 | 'gist_earth', 'gist_gray', 'gist_grey', 'gist_heat', 'gist_ncar', 'gist_rainbow', 23 | 'gist_stern', 'gist_yarg', 'gist_yerg', 'gnuplot', 'gnuplot2', 24 | 'hot', 'hsv', 'inferno', 'jet', 'magma', 'nipy_spectral', 'ocean', 'pink', 'plasma', 25 | 'prism', 'rainbow', 'seismic', 'spring', 'summer', 'tab10', 'tab20', 'tab20b', 'tab20c', 26 | 'terrain', 'turbo', 'twilight', 'twilight_shifted', 'winter' 27 | ] 28 | 29 | default_text = 'demo of word cloud for ComfyUI by dzNodes' 30 | font_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.normpath(__file__))), 'font') 31 | ini_file = os.path.join(os.path.dirname(os.path.dirname(os.path.normpath(__file__))), "font_dir.ini") 32 | 33 | try: 34 | with open(ini_file, 'r') as f: 35 | ini = f.read() 36 | d = ini[ini.find('=') + 1:].rstrip().lstrip() 37 | if os.path.exists(d): 38 | font_dir = d 39 | else: 40 | log(f'ERROR: invalid dir, default to be used. check {ini_file}') 41 | except Exception as e: 42 | log(f'ERROR: {ini_file} ' + repr(e)) 43 | 44 | file_list = glob.glob(font_dir + '/*.ttf') 45 | file_list.extend(glob.glob(font_dir + '/*.otf')) 46 | font_dict = {} 47 | for i in range(len(file_list)): 48 | _, filename = os.path.split(file_list[i]) 49 | font_dict[filename] = file_list[i] 50 | font_list = list(font_dict.keys()) 51 | log(f'find {len(font_list)} fonts in {font_dir}') 52 | 53 | # Tensor to PIL 54 | def tensor2pil(image): 55 | return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) 56 | 57 | # PIL to Tensor 58 | def pil2tensor(image): 59 | return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) 60 | 61 | def getRGBAmask(image): 62 | ret_mask = torch.tensor([pil2tensor(image)[0, :, :, 3].tolist()]) 63 | return ret_mask 64 | 65 | def img_whitebackground(image): 66 | if image.mode != 'RGBA': 67 | image = image.convert('RGBA') 68 | width = image.width 69 | height = image.height 70 | img_new = Image.new('RGB', size=(width, height), color=(255, 255, 255)) 71 | img_new.paste(image, (0, 0), mask=image) 72 | return img_new 73 | 74 | class ComfyWordCloud: 75 | 76 | def __init__(self): 77 | pass 78 | 79 | @classmethod 80 | def INPUT_TYPES(self): 81 | 82 | return { 83 | "required": { 84 | "text": ("STRING", {"default": "", "multiline": True}), # 文本内容 85 | ## size 86 | "width": ("INT", {"default": 512}), # 画幅宽 87 | "height": ("INT", {"default": 512}), # 画幅高 88 | "scale": ("FLOAT", {"default": 1, "min": 0.1, "max": 1000.0, "step": 0.01}), # 放大倍数 89 | "margin": ("INT", {"default": 0}), # 空白边界 90 | ## font 91 | "font_path": (font_list,), # 字体文件 92 | "min_font_size": ("INT", {"default": 4}), # 单词最小size 93 | "max_font_size": ("INT", {"default": 128}), # 单词最大size 94 | "relative_scaling": ("FLOAT", {"default": 0.5, "min": 0.01, "max": 1.0, "step": 0.01}), # 单词大小离散度 95 | ## color control 96 | "colormap": (COLOR_MAP,), # 文字颜色 97 | "background_color": ("STRING", {"default": "#FFFFFF"}), # 背景颜色 98 | "transparent_background": ("BOOLEAN", {"default": True}), # 是否透明,如果是则需要background_color强制为None 99 | ## word control 100 | "prefer_horizontal": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.01}), # 横排比例 101 | "max_words": ("INT", {"default": 200}), # 最大单词数量 102 | "repeat": ("BOOLEAN", {"default": False}), # 允许重复单词直到最大单词数量 103 | "include_numbers": ("BOOLEAN", {"default": False}), # 是否包含数字 104 | "random_state": ("INT", {"default": -1, "min": -1, "max": 0xffffffffffffffff}), # 固定随机值,-1时强制转为None(随机) 105 | "stopwords": ("STRING", {"default": ""}), # 排除词,用中英文逗号或空格分开 106 | }, 107 | "optional": { 108 | ## recolor refrence image 109 | "color_ref_image": ("IMAGE", ), 110 | ## mask image 白底或带alpha通道 111 | "mask_image": ("IMAGE", ), # 有输入mask则强制使用该图尺寸 112 | "contour_width": ("FLOAT", {"default": 0, "min": 0, "max": 9999, "step": 0.1}), 113 | "contour_color": ("STRING", {"default": "#000000"}), 114 | "keynote_words": ("STRING", {"default": ""}), # 重点词,用中英文逗号或空格分开 115 | "keynote_weight": ("INT", {"default": 60}), # 重点词加权 116 | } 117 | } 118 | 119 | RETURN_TYPES = ("IMAGE", "MASK",) 120 | RETURN_NAMES = ("image", "mask",) 121 | FUNCTION = 'wordcloud' 122 | CATEGORY = '😺dzNodes/WordCloud' 123 | OUTPUT_NODE = True 124 | 125 | def wordcloud(self, text, width, height, margin, scale, font_path, 126 | min_font_size, max_font_size, relative_scaling, 127 | colormap, background_color, transparent_background, 128 | prefer_horizontal, max_words, repeat, 129 | include_numbers, random_state, stopwords, 130 | color_ref_image=None, mask_image=None, 131 | contour_width=None, contour_color=None, 132 | keynote_words=None, keynote_weight=None, 133 | ): 134 | 135 | # parameter preprocessing 136 | if text == '': 137 | text = default_text 138 | log(f"text input not found, use demo string.") 139 | 140 | freq_dict = WordCloud().process_text(' '.join(jieba.cut(text))) 141 | if not keynote_words == '': 142 | keynote_list = list(re.split(r'[,,\s*]', keynote_words)) 143 | keynote_list = [x for x in keynote_list if x != ''] # 去除空字符 144 | keynote_dict = {keynote_list[i]: keynote_weight + max(freq_dict.values()) for i in range(len(keynote_list))} 145 | freq_dict.update(keynote_dict) 146 | log(f"word frequencies dict generated, include {len(freq_dict)} words.") 147 | 148 | 149 | font_path = font_dict[font_path] 150 | if not os.path.exists(font_path): 151 | font_path = os.path.join(os.path.join(os.path.dirname(os.path.dirname(os.path.normpath(__file__))), 'font'), 152 | 'Alibaba-PuHuiTi-Heavy.ttf') 153 | log(f"font_path not found, use {font_path}") 154 | else: 155 | log(f"font_path = {font_path}") 156 | 157 | stopwords_set = set("") 158 | if not stopwords == "": 159 | stopwords_list = re.split(r'[,,\s*]', stopwords) 160 | stopwords_set = set([x for x in stopwords_list if x != '']) # 去除空字符 161 | 162 | # 同时在词典中删除(stopwords之bug) 163 | for item in stopwords_set: 164 | if item in freq_dict.keys(): 165 | del freq_dict[item] 166 | 167 | bg_color = background_color 168 | mode = 'RGB' 169 | if transparent_background: 170 | bg_color = None 171 | mode = 'RGBA' 172 | 173 | if random_state == -1: 174 | random_state = None 175 | 176 | mask = None 177 | image_width = width 178 | image_height = height 179 | if not mask_image == None: 180 | p_mask = tensor2pil(mask_image) 181 | mask = np.array(img_whitebackground(p_mask)) 182 | image_width = p_mask.width 183 | image_height = p_mask.height 184 | 185 | 186 | # set wordcloud parameters 187 | wc = WordCloud(width=width, height=height, scale=scale, margin=margin, 188 | font_path=font_path, min_font_size=min_font_size, max_font_size=max_font_size, 189 | relative_scaling=relative_scaling, colormap=colormap, mode=mode, 190 | background_color=bg_color, prefer_horizontal=prefer_horizontal, 191 | max_words=max_words, repeat=repeat, include_numbers=include_numbers, 192 | random_state=random_state, stopwords=stopwords_set, 193 | mask=mask, contour_width=contour_width, contour_color=contour_color, 194 | ) 195 | 196 | # generate wordcloud 197 | wc.generate_from_frequencies(freq_dict) 198 | 199 | # generate recolor 200 | if not color_ref_image == None: 201 | p_color_ref_image = tensor2pil(color_ref_image) 202 | p_color_ref_image = p_color_ref_image.resize((image_width, image_height)) 203 | image_colors = ImageColorGenerator(np.array(p_color_ref_image)) 204 | wc.recolor(color_func=image_colors) 205 | 206 | ret_image = wc.to_image().convert('RGBA') 207 | ret_mask = getRGBAmask(ret_image) 208 | 209 | return (pil2tensor(ret_image), ret_mask,) 210 | 211 | 212 | NODE_CLASS_MAPPINGS = { 213 | "ComfyWordCloud": ComfyWordCloud 214 | } 215 | 216 | NODE_DISPLAY_NAME_MAPPINGS = { 217 | "ComfyWordCloud": "Word Cloud" 218 | } -------------------------------------------------------------------------------- /py/load_textfile.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | class LoadTextFile: 4 | 5 | def __init__(self): 6 | pass 7 | 8 | @classmethod 9 | def INPUT_TYPES(cls): 10 | return { 11 | "required": { 12 | "path": ("STRING", {"default": 'c:\\text.txt'}), 13 | }, 14 | "optional": { 15 | }, 16 | } 17 | 18 | 19 | RETURN_TYPES = ("STRING",) 20 | RETURN_NAMES = ("Text",) 21 | FUNCTION = "load_text_file" 22 | OUTPUT_NODE = True 23 | CATEGORY = '😺dzNodes/WordCloud' 24 | 25 | def load_text_file(self, path): 26 | 27 | text_content = "" 28 | try: 29 | with open(os.path.normpath(path), 'r', encoding="utf-8") as f: 30 | text_content = ''.join(str(l) for l in f.read()) 31 | print("# 😺dzNodes: Load Text File -> " + path + " success.") 32 | except Exception as e: 33 | print("# 😺dzNodes: Load Text File -> ERROR, " + path + ", " + repr(e)) 34 | 35 | return {"ui": {"text":text_content}, "result": (text_content,)} 36 | 37 | 38 | NODE_CLASS_MAPPINGS = { 39 | "LoadTextFile": LoadTextFile 40 | } 41 | NODE_DISPLAY_NAME_MAPPINGS = { 42 | "LoadTextFile": "Load Text File" 43 | } -------------------------------------------------------------------------------- /py/rgb_picker.py: -------------------------------------------------------------------------------- 1 | 2 | mode_list = ['HEX', 'DEC'] 3 | 4 | def hex_to_dec(inhex): 5 | rval = inhex[1:3] 6 | gval = inhex[3:5] 7 | bval = inhex[5:] 8 | rgbval = (int(rval, 16), int(gval, 16), int(bval, 16)) 9 | return rgbval 10 | 11 | class RGB_Picker: 12 | 13 | def __init__(self): 14 | pass 15 | 16 | @classmethod 17 | def INPUT_TYPES(self): 18 | 19 | return { 20 | "required": { 21 | "color": ("COLOR", {"default": "white"},), 22 | "mode": (mode_list,), # 输出模式 23 | }, 24 | "optional": { 25 | } 26 | } 27 | 28 | RETURN_TYPES = ("STRING",) 29 | RETURN_NAMES = ("value",) 30 | FUNCTION = 'picker' 31 | CATEGORY = '😺dzNodes/WordCloud' 32 | OUTPUT_NODE = True 33 | 34 | def picker(self, color, mode,): 35 | ret = color 36 | if mode == 'DEC': 37 | ret = hex_to_dec(color) 38 | return (ret,) 39 | 40 | 41 | NODE_CLASS_MAPPINGS = { 42 | "RGB_Picker": RGB_Picker 43 | } 44 | 45 | NODE_DISPLAY_NAME_MAPPINGS = { 46 | "RGB_Picker": "RGB Color Picker" 47 | } -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "comfyui_wordcloud" 3 | description = "Nodes:Word Cloud, Load Text File" 4 | version = "1.0.0" 5 | license = "LICENSE" 6 | dependencies = ["numpy", "pillow", "torch", "matplotlib", "wordcloud", "jieba"] 7 | 8 | [project.urls] 9 | Repository = "https://github.com/chflame163/ComfyUI_WordCloud" 10 | # Used by Comfy Registry https://comfyregistry.org 11 | 12 | [tool.comfy] 13 | PublisherId = "chflame163" 14 | DisplayName = "ComfyUI_WordCloud" 15 | Icon = "" 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | pillow 3 | torch 4 | matplotlib 5 | wordcloud 6 | jieba -------------------------------------------------------------------------------- /workflow/comfy_wordcloud_advance.json: -------------------------------------------------------------------------------- 1 | { 2 | "last_node_id": 18, 3 | "last_link_id": 42, 4 | "nodes": [ 5 | { 6 | "id": 15, 7 | "type": "PreviewImage", 8 | "pos": [ 9 | 1377, 10 | 29 11 | ], 12 | "size": { 13 | "0": 326.0002136230469, 14 | "1": 305.6000671386719 15 | }, 16 | "flags": {}, 17 | "order": 6, 18 | "mode": 0, 19 | "inputs": [ 20 | { 21 | "name": "images", 22 | "type": "IMAGE", 23 | "link": 37 24 | } 25 | ], 26 | "properties": { 27 | "Node name for S&R": "PreviewImage" 28 | } 29 | }, 30 | { 31 | "id": 1, 32 | "type": "LoadTextFile", 33 | "pos": [ 34 | 592, 35 | 45 36 | ], 37 | "size": { 38 | "0": 289.6014709472656, 39 | "1": 58 40 | }, 41 | "flags": {}, 42 | "order": 0, 43 | "mode": 0, 44 | "outputs": [ 45 | { 46 | "name": "Text", 47 | "type": "STRING", 48 | "links": [ 49 | 21 50 | ], 51 | "shape": 3, 52 | "slot_index": 0 53 | } 54 | ], 55 | "properties": { 56 | "Node name for S&R": "LoadTextFile" 57 | }, 58 | "widgets_values": [ 59 | "C:\\AIRes\\test2.txt" 60 | ] 61 | }, 62 | { 63 | "id": 3, 64 | "type": "LoadImage", 65 | "pos": [ 66 | 586, 67 | 164 68 | ], 69 | "size": { 70 | "0": 295.9548645019531, 71 | "1": 314 72 | }, 73 | "flags": {}, 74 | "order": 1, 75 | "mode": 0, 76 | "outputs": [ 77 | { 78 | "name": "IMAGE", 79 | "type": "IMAGE", 80 | "links": [ 81 | 13, 82 | 19, 83 | 27 84 | ], 85 | "shape": 3, 86 | "slot_index": 0 87 | }, 88 | { 89 | "name": "MASK", 90 | "type": "MASK", 91 | "links": null, 92 | "shape": 3 93 | } 94 | ], 95 | "properties": { 96 | "Node name for S&R": "LoadImage" 97 | }, 98 | "widgets_values": [ 99 | "D04QPvc.jpg", 100 | "image" 101 | ] 102 | }, 103 | { 104 | "id": 11, 105 | "type": "Image Rembg (Remove Background)", 106 | "pos": [ 107 | 584, 108 | 541 109 | ], 110 | "size": { 111 | "0": 315, 112 | "1": 250 113 | }, 114 | "flags": {}, 115 | "order": 3, 116 | "mode": 0, 117 | "inputs": [ 118 | { 119 | "name": "images", 120 | "type": "IMAGE", 121 | "link": 27 122 | } 123 | ], 124 | "outputs": [ 125 | { 126 | "name": "images", 127 | "type": "IMAGE", 128 | "links": [ 129 | 28 130 | ], 131 | "shape": 3, 132 | "slot_index": 0 133 | } 134 | ], 135 | "properties": { 136 | "Node name for S&R": "Image Rembg (Remove Background)" 137 | }, 138 | "widgets_values": [ 139 | true, 140 | "u2net", 141 | false, 142 | false, 143 | false, 144 | 240, 145 | 10, 146 | 10, 147 | "none" 148 | ] 149 | }, 150 | { 151 | "id": 14, 152 | "type": "RGB_Picker", 153 | "pos": [ 154 | 941, 155 | 26 156 | ], 157 | "size": { 158 | "0": 210, 159 | "1": 94 160 | }, 161 | "flags": {}, 162 | "order": 2, 163 | "mode": 0, 164 | "outputs": [ 165 | { 166 | "name": "value", 167 | "type": "STRING", 168 | "links": [ 169 | 36 170 | ], 171 | "shape": 3, 172 | "slot_index": 0 173 | } 174 | ], 175 | "properties": { 176 | "Node name for S&R": "RGB_Picker" 177 | }, 178 | "widgets_values": [ 179 | "#ffffff", 180 | "HEX" 181 | ] 182 | }, 183 | { 184 | "id": 9, 185 | "type": "ComfyWordCloud", 186 | "pos": [ 187 | 940, 188 | 177 189 | ], 190 | "size": { 191 | "0": 394.4380187988281, 192 | "1": 623.7396850585938 193 | }, 194 | "flags": {}, 195 | "order": 4, 196 | "mode": 0, 197 | "inputs": [ 198 | { 199 | "name": "color_ref_image", 200 | "type": "IMAGE", 201 | "link": 19 202 | }, 203 | { 204 | "name": "mask_image", 205 | "type": "IMAGE", 206 | "link": 28 207 | }, 208 | { 209 | "name": "text", 210 | "type": "STRING", 211 | "link": 21, 212 | "widget": { 213 | "name": "text" 214 | } 215 | }, 216 | { 217 | "name": "background_color", 218 | "type": "STRING", 219 | "link": 36, 220 | "widget": { 221 | "name": "background_color" 222 | } 223 | } 224 | ], 225 | "outputs": [ 226 | { 227 | "name": "image", 228 | "type": "IMAGE", 229 | "links": [ 230 | 23, 231 | 37, 232 | 42 233 | ], 234 | "shape": 3, 235 | "slot_index": 0 236 | }, 237 | { 238 | "name": "mask", 239 | "type": "MASK", 240 | "links": null, 241 | "shape": 3 242 | } 243 | ], 244 | "properties": { 245 | "Node name for S&R": "ComfyWordCloud" 246 | }, 247 | "widgets_values": [ 248 | "", 249 | 512, 250 | 512, 251 | 4, 252 | 0, 253 | "锐字真言体RZZhenYan.ttf", 254 | 4, 255 | 128, 256 | 0.5, 257 | "Oranges", 258 | "#000000", 259 | true, 260 | 0.9, 261 | 200, 262 | false, 263 | false, 264 | -1, 265 | "的,是,再", 266 | 0, 267 | "#000000", 268 | "重,大,新,闻", 269 | 60 270 | ] 271 | }, 272 | { 273 | "id": 13, 274 | "type": "SaveImage", 275 | "pos": [ 276 | 1731, 277 | 35 278 | ], 279 | "size": [ 280 | 678.1050823120122, 281 | 754.5836868286134 282 | ], 283 | "flags": {}, 284 | "order": 9, 285 | "mode": 0, 286 | "inputs": [ 287 | { 288 | "name": "images", 289 | "type": "IMAGE", 290 | "link": 40 291 | } 292 | ], 293 | "properties": {}, 294 | "widgets_values": [ 295 | "ComfyUI" 296 | ] 297 | }, 298 | { 299 | "id": 8, 300 | "type": "GetImageSize", 301 | "pos": [ 302 | 1383, 303 | 391 304 | ], 305 | "size": { 306 | "0": 210, 307 | "1": 46 308 | }, 309 | "flags": {}, 310 | "order": 5, 311 | "mode": 0, 312 | "inputs": [ 313 | { 314 | "name": "image", 315 | "type": "IMAGE", 316 | "link": 23 317 | } 318 | ], 319 | "outputs": [ 320 | { 321 | "name": "width", 322 | "type": "INT", 323 | "links": [ 324 | 15 325 | ], 326 | "shape": 3, 327 | "slot_index": 0 328 | }, 329 | { 330 | "name": "height", 331 | "type": "INT", 332 | "links": [ 333 | 16 334 | ], 335 | "shape": 3, 336 | "slot_index": 1 337 | } 338 | ], 339 | "properties": { 340 | "Node name for S&R": "GetImageSize" 341 | } 342 | }, 343 | { 344 | "id": 7, 345 | "type": "ImageScale", 346 | "pos": [ 347 | 1379, 348 | 493 349 | ], 350 | "size": { 351 | "0": 315, 352 | "1": 130 353 | }, 354 | "flags": {}, 355 | "order": 7, 356 | "mode": 0, 357 | "inputs": [ 358 | { 359 | "name": "image", 360 | "type": "IMAGE", 361 | "link": 13 362 | }, 363 | { 364 | "name": "width", 365 | "type": "INT", 366 | "link": 15, 367 | "widget": { 368 | "name": "width" 369 | } 370 | }, 371 | { 372 | "name": "height", 373 | "type": "INT", 374 | "link": 16, 375 | "widget": { 376 | "name": "height" 377 | } 378 | } 379 | ], 380 | "outputs": [ 381 | { 382 | "name": "IMAGE", 383 | "type": "IMAGE", 384 | "links": [ 385 | 41 386 | ], 387 | "shape": 3, 388 | "slot_index": 0 389 | } 390 | ], 391 | "properties": { 392 | "Node name for S&R": "ImageScale" 393 | }, 394 | "widgets_values": [ 395 | "nearest-exact", 396 | 512, 397 | 512, 398 | "disabled" 399 | ] 400 | }, 401 | { 402 | "id": 18, 403 | "type": "Image Blending Mode", 404 | "pos": [ 405 | 1379, 406 | 684 407 | ], 408 | "size": { 409 | "0": 315, 410 | "1": 102 411 | }, 412 | "flags": {}, 413 | "order": 8, 414 | "mode": 0, 415 | "inputs": [ 416 | { 417 | "name": "image_a", 418 | "type": "IMAGE", 419 | "link": 42 420 | }, 421 | { 422 | "name": "image_b", 423 | "type": "IMAGE", 424 | "link": 41 425 | } 426 | ], 427 | "outputs": [ 428 | { 429 | "name": "image", 430 | "type": "IMAGE", 431 | "links": [ 432 | 40 433 | ], 434 | "shape": 3, 435 | "slot_index": 0 436 | } 437 | ], 438 | "properties": { 439 | "Node name for S&R": "Image Blending Mode" 440 | }, 441 | "widgets_values": [ 442 | "add", 443 | 0.55 444 | ] 445 | } 446 | ], 447 | "links": [ 448 | [ 449 | 13, 450 | 3, 451 | 0, 452 | 7, 453 | 0, 454 | "IMAGE" 455 | ], 456 | [ 457 | 15, 458 | 8, 459 | 0, 460 | 7, 461 | 1, 462 | "INT" 463 | ], 464 | [ 465 | 16, 466 | 8, 467 | 1, 468 | 7, 469 | 2, 470 | "INT" 471 | ], 472 | [ 473 | 19, 474 | 3, 475 | 0, 476 | 9, 477 | 0, 478 | "IMAGE" 479 | ], 480 | [ 481 | 21, 482 | 1, 483 | 0, 484 | 9, 485 | 2, 486 | "STRING" 487 | ], 488 | [ 489 | 23, 490 | 9, 491 | 0, 492 | 8, 493 | 0, 494 | "IMAGE" 495 | ], 496 | [ 497 | 27, 498 | 3, 499 | 0, 500 | 11, 501 | 0, 502 | "IMAGE" 503 | ], 504 | [ 505 | 28, 506 | 11, 507 | 0, 508 | 9, 509 | 1, 510 | "IMAGE" 511 | ], 512 | [ 513 | 36, 514 | 14, 515 | 0, 516 | 9, 517 | 3, 518 | "STRING" 519 | ], 520 | [ 521 | 37, 522 | 9, 523 | 0, 524 | 15, 525 | 0, 526 | "IMAGE" 527 | ], 528 | [ 529 | 40, 530 | 18, 531 | 0, 532 | 13, 533 | 0, 534 | "IMAGE" 535 | ], 536 | [ 537 | 41, 538 | 7, 539 | 0, 540 | 18, 541 | 1, 542 | "IMAGE" 543 | ], 544 | [ 545 | 42, 546 | 9, 547 | 0, 548 | 18, 549 | 0, 550 | "IMAGE" 551 | ] 552 | ], 553 | "groups": [], 554 | "config": {}, 555 | "extra": {}, 556 | "version": 0.4 557 | } -------------------------------------------------------------------------------- /workflow/comfy_wordcloud_simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "last_node_id": 2, 3 | "last_link_id": 1, 4 | "nodes": [ 5 | { 6 | "id": 2, 7 | "type": "PreviewImage", 8 | "pos": [ 9 | 1488, 10 | 305 11 | ], 12 | "size": { 13 | "0": 398.4970397949219, 14 | "1": 362.7762756347656 15 | }, 16 | "flags": {}, 17 | "order": 1, 18 | "mode": 0, 19 | "inputs": [ 20 | { 21 | "name": "images", 22 | "type": "IMAGE", 23 | "link": 1 24 | } 25 | ], 26 | "properties": { 27 | "Node name for S&R": "PreviewImage" 28 | } 29 | }, 30 | { 31 | "id": 1, 32 | "type": "ComfyWordCloud", 33 | "pos": [ 34 | 971, 35 | 304 36 | ], 37 | "size": { 38 | "0": 439.3404235839844, 39 | "1": 655.6005859375 40 | }, 41 | "flags": {}, 42 | "order": 0, 43 | "mode": 0, 44 | "inputs": [ 45 | { 46 | "name": "color_ref_image", 47 | "type": "IMAGE", 48 | "link": null 49 | }, 50 | { 51 | "name": "mask_image", 52 | "type": "IMAGE", 53 | "link": null 54 | } 55 | ], 56 | "outputs": [ 57 | { 58 | "name": "image", 59 | "type": "IMAGE", 60 | "links": [ 61 | 1 62 | ], 63 | "shape": 3, 64 | "slot_index": 0 65 | }, 66 | { 67 | "name": "mask", 68 | "type": "MASK", 69 | "links": null, 70 | "shape": 3 71 | } 72 | ], 73 | "properties": { 74 | "Node name for S&R": "ComfyWordCloud" 75 | }, 76 | "widgets_values": [ 77 | "据国家铁路局消息,近年来,随着铁路建设不断推进,我国铁路网越织越密,“八纵八横”高速铁路网主通道已建成约80%,普速铁路网不断完善。与此同时,铁路运输服务正由“走得了”“运得出”向“走得好”“运得畅”转变。我国铁路客运周转量、货物发送量、货运周转量以及运输密度均居世界首位;复兴号实现对31个省份全覆盖;客运服务市场化、便利化、信息化加速推进,建成世界规模最大的铁路互联网售票系统;货运产品供给不断优化,重载运输、快运货物班列、集装箱、冷链运输、高铁快运全面发展,实现运输服务品质全面跃升。", 78 | 512, 79 | 512, 80 | 1, 81 | 0, 82 | "Alibaba-PuHuiTi-Heavy.ttf", 83 | 4, 84 | 128, 85 | 0.5, 86 | "viridis", 87 | "#FFFFFF", 88 | false, 89 | 1, 90 | 200, 91 | false, 92 | false, 93 | -1, 94 | "", 95 | 0, 96 | "#000000", 97 | "重 大 新 闻", 98 | 60 99 | ] 100 | } 101 | ], 102 | "links": [ 103 | [ 104 | 1, 105 | 1, 106 | 0, 107 | 2, 108 | 0, 109 | "IMAGE" 110 | ] 111 | ], 112 | "groups": [], 113 | "config": {}, 114 | "extra": {}, 115 | "version": 0.4 116 | } --------------------------------------------------------------------------------