├── README.md ├── __init__.py ├── fonts ├── simhei.ttf └── simkai.ttf ├── image ├── meyo01.png ├── meyo02.png ├── meyo03.png ├── meyo04.png ├── meyo05.png ├── meyo06.png └── v.jpg ├── meyo_node_Computational.py ├── meyo_node_File.py ├── meyo_node_Functional.py ├── meyo_node_String.py └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | # ComfyUI_StringOps 2 | **Efficient Text Processing Tool for ComfyUI Workflow** 3 | 4 | ComfyUI_StringOps is an advanced text processing tool tailored for the ComfyUI workflow, offering robust text manipulation capabilities to address core issues such as text preprocessing, data transformation, and batch automation, enabling efficient and precise operations. 5 | 6 | - **Batch Replacement:** 7 | Supports partial or global batch replacement of strings for rapid text processing and delivery to subsequent processes, enhancing task efficiency. 8 | 9 | - **Intelligent Splitting/Concatenation:** 10 | Splits text based on character counts and delimiters, and supports flexible recombination of multiple text segments to meet complex processing needs. 11 | 12 | - **Precise Extraction:** 13 | Accurately extracts specified content from text using positional indexing and keyword matching, providing precise data support for subsequent processes. 14 | 15 | - **Integration with Excel:** 16 | Achieves deep integration with Excel, supporting read and write operations on paged and tabular data, facilitating batch completion of complex tasks and significantly improving efficiency. 17 | 18 | ## Update Notes (2025-06-05): 19 | 1.Added LoadAndAdjustImage node to load and resize images. 20 | 21 | 2.Added SaveImagEX node to save images to a specified directory (also added an image output port). 22 | 23 | 3.Added functionality to get the current timestamp (can be used as a random seed). 24 | 25 | 4.Added the ability to copy and cut files. 26 | 27 | 5.Added the feature of reading web page content. 28 | 29 | 6.Added text layout to image node, image layer overlay node. 30 | 31 | ## Partial Node Display 32 | **Text Processing Node** 33 | ![image](image/meyo01.png) 34 | ![image](image/meyo02.png) 35 | ![image](image/meyo03.png) 36 | ![image](image/meyo04.png) 37 | ![image](image/meyo05.png) 38 | 39 | **Table Data Read/Write Nodes** 40 | ![image](image/meyo06.png) 41 | 42 | ComfyUI_StringOps excels in handling prompts within workflows, especially in organizing and optimizing them, thereby significantly enhancing efficiency and accuracy. 43 | 44 | ## Install 45 | ```cd ComfyUI/custom_nodes``` 46 | 47 | ```git clone https://github.com/MeeeyoAI/ComfyUI_StringOps.git``` 48 | 49 | Or download the zip file and extracted, copy the resulting folder to ```ComfyUI\custom_nodes``` Restart ComfyUI. 50 | 51 | ## More Info 52 | ✅ `bilibili:` https://space.bilibili.com/3546690300676691 53 | 54 | ✅ `Website:` http://www.meeeyo.com 55 | 56 | ✅ `Sponsorship:` https://t.zsxq.com/ufSS2 57 | 58 | ✅ `WeChat`: meeeyo 59 | 60 | ![image](image/v.jpg) 61 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | import base64 2 | note = base64.b64decode("44CQ6KeF6bG8QUnnu5jnlLvjgJHlpoLpnIDmm7TlpJrluK7liqnmiJbllYbliqHpnIDmsYIgK3Z4OiBtZWVleW8=").decode('utf-8') 3 | class AnyType(str): 4 | def __ne__(self, __value: object) -> bool: 5 | return False 6 | any_typ = AnyType("*") 7 | from .meyo_node_Computational import * 8 | from .meyo_node_String import * 9 | from .meyo_node_File import * 10 | from .meyo_node_Functional import * 11 | 12 | 13 | NODE_CLASS_MAPPINGS = { 14 | 15 | #运算型节点:meyo_node_Computational 16 | "CompareInt": CompareInt, 17 | "FloatToInteger": FloatToInteger, 18 | "GenerateNumbers": GenerateNumbers, 19 | "GetRandomIntegerInRange": GetRandomIntegerInRange, 20 | 21 | 22 | #字符串处理:meyo_node_String 23 | "SingleTextInput": SingleTextInput, 24 | "TextToList": TextToList, 25 | "TextConcatenator": TextConcatenator, 26 | "MultiParamInputNode": MultiParamInputNode, 27 | "NumberExtractor": NumberExtractor, 28 | "AddPrefixSuffix": AddPrefixSuffix, 29 | "ExtractSubstring": ExtractSubstring, 30 | "ExtractSubstringByIndices": ExtractSubstringByIndices, 31 | "SplitStringByDelimiter": SplitStringByDelimiter, 32 | "ProcessString": ProcessString, 33 | "ExtractBeforeAfter": ExtractBeforeAfter, 34 | "SimpleTextReplacer": SimpleTextReplacer, 35 | "ReplaceNthOccurrence": ReplaceNthOccurrence, 36 | "ReplaceMultiple": ReplaceMultiple, 37 | "BatchReplaceStrings": BatchReplaceStrings, 38 | "RandomLineFromText": RandomLineFromText, 39 | "CheckSubstringPresence": CheckSubstringPresence, 40 | "AddPrefixSuffixToLines": AddPrefixSuffixToLines, 41 | "ExtractAndCombineLines": ExtractAndCombineLines, 42 | "FilterLinesBySubstrings": FilterLinesBySubstrings, 43 | "FilterLinesByWordCount": FilterLinesByWordCount, 44 | "SplitAndExtractText": SplitAndExtractText, 45 | "CountOccurrences": CountOccurrences, 46 | "ExtractLinesByIndex": ExtractLinesByIndex, 47 | "ExtractSpecificLines": ExtractSpecificLines, 48 | "RemoveContentBetweenChars": RemoveContentBetweenChars, 49 | "ShuffleTextLines": ShuffleTextLines, 50 | "ConditionalTextOutput": ConditionalTextOutput, 51 | "TextConditionCheck": TextConditionCheck, 52 | "TextConcatenation": TextConcatenation, 53 | "ExtractSpecificData": ExtractSpecificData, 54 | "FindFirstLineContent": FindFirstLineContent, 55 | "GetIntParam": GetIntParam, 56 | "GetFloatParam": GetFloatParam, 57 | "GenerateVideoPrompt": GenerateVideoPrompt, 58 | 59 | #文件处理:meyo_node_File 60 | "LoadAndAdjustImage": LoadAndAdjustImage, 61 | "GenericImageLoader": GenericImageLoader, 62 | "ImageAdjuster": ImageAdjuster, 63 | "CustomCrop": CustomCrop, 64 | "SaveImagEX": SaveImagEX, 65 | "FileCopyCutNode": FileCopyCutNode, 66 | "FileNameReplacer": FileNameReplacer, 67 | "WriteToTxtFile": WriteToTxtFile, 68 | "FileDeleteNode": FileDeleteNode, 69 | "FileListAndSuffix": FileListAndSuffix, 70 | "ImageOverlayAlignment": ImageOverlayAlignment, 71 | "TextToImage": TextToImage, 72 | 73 | "ReadExcelData": ReadExcelData, 74 | "WriteExcelData": WriteExcelData, 75 | "WriteExcelImage": WriteExcelImage, 76 | "FindExcelData": FindExcelData, 77 | "ReadExcelRowOrColumnDiff": ReadExcelRowOrColumnDiff, 78 | 79 | #功能型节点:meyo_node_Functional 80 | "GetCurrentTime": GetCurrentTime, 81 | "SimpleRandomSeed": SimpleRandomSeed, 82 | "SelectionParameter": SelectionParameter, 83 | "ReadWebNode": ReadWebNode, 84 | "DecodePreview": DecodePreview, 85 | } 86 | 87 | 88 | NODE_DISPLAY_NAME_MAPPINGS = { 89 | 90 | #运算型节点:meyo_node_Computational 91 | "CompareInt": "比较数值🐠meeeyo.com", 92 | "FloatToInteger": "规范数值🐠meeeyo.com", 93 | "GenerateNumbers": "生成范围数组🐠meeeyo.com", 94 | "GetRandomIntegerInRange": "范围内随机数🐠meeeyo.com", 95 | 96 | #字符串处理:meyo_node_String 97 | "SingleTextInput": "文本输入🐠meeeyo.com", 98 | "TextToList": "文本到列表🐠meeeyo.com", 99 | "TextConcatenator": "文本拼接🐠meeeyo.com", 100 | "MultiParamInputNode": "多参数输入🐠meeeyo.com", 101 | "NumberExtractor": "整数参数🐠meeeyo.com", 102 | "AddPrefixSuffix": "添加前后缀🐠meeeyo.com", 103 | "ExtractSubstring": "提取标签之间🐠meeeyo.com", 104 | "ExtractSubstringByIndices": "按数字范围提取🐠meeeyo.com", 105 | "SplitStringByDelimiter": "分隔符拆分两边🐠meeeyo.com", 106 | "ProcessString": "常规处理字符🐠meeeyo.com", 107 | "ExtractBeforeAfter": "提取前后字符🐠meeeyo.com", 108 | "SimpleTextReplacer": "简易文本替换🐠meeeyo.com", 109 | "ReplaceNthOccurrence": "替换第n次出现🐠meeeyo.com", 110 | "ReplaceMultiple": "多次出现依次替换🐠meeeyo.com", 111 | "BatchReplaceStrings": "批量替换字符🐠meeeyo.com", 112 | "RandomLineFromText": "随机行内容🐠meeeyo.com", 113 | "CheckSubstringPresence": "判断是否包含字符🐠meeeyo.com", 114 | "AddPrefixSuffixToLines": "段落每行添加前后缀🐠meeeyo.com", 115 | "ExtractAndCombineLines": "段落提取指定索引行🐠meeeyo.com", 116 | "FilterLinesBySubstrings": "段落提取或移除字符行🐠meeeyo.com", 117 | "FilterLinesByWordCount": "段落字数条件过滤行🐠meeeyo.com", 118 | "SplitAndExtractText": "按序号提取分割文本🐠meeeyo.com", 119 | "CountOccurrences": "文本出现次数🐠meeeyo.com", 120 | "ExtractLinesByIndex": "文本拆分🐠meeeyo.com", 121 | "ExtractSpecificLines": "提取特定行🐠meeeyo.com", 122 | "RemoveContentBetweenChars": "删除标签内的内容🐠meeeyo.com", 123 | "ShuffleTextLines": "随机打乱🐠meeeyo.com", 124 | "ConditionalTextOutput": "判断返回内容🐠meeeyo.com", 125 | "TextConditionCheck": "文本按条件判断🐠meeeyo.com", 126 | "TextConcatenation": "文本组合🐠meeeyo.com", 127 | "ExtractSpecificData": "提取多层指定数据🐠meeeyo.com", 128 | "FindFirstLineContent": "指定字符行参数🐠meeeyo.com", 129 | "GetIntParam": "获取整数🐠meeeyo.com", 130 | "GetFloatParam": "获取浮点数🐠meeeyo.com", 131 | "GenerateVideoPrompt": "视频指令词模板🐠meeeyo.com", 132 | 133 | #文件处理:meyo_node_File 134 | "LoadAndAdjustImage": "加载重置图像🐠meeeyo.com", 135 | "GenericImageLoader": "全能加载图像🐠meeeyo.com", 136 | "ImageAdjuster": "重置图像🐠meeeyo.com", 137 | "CustomCrop": "裁剪图像🐠meeeyo.com", 138 | "SaveImagEX": "保存图像🐠meeeyo.com", 139 | "FileCopyCutNode": "文件操作🐠meeeyo.com", 140 | "FileNameReplacer": "替换文件名🐠meeeyo.com", 141 | "WriteToTxtFile": "文本写入TXT🐠meeeyo.com", 142 | "FileDeleteNode": "清理文件🐠meeeyo.com", 143 | "FileListAndSuffix": "从路径加载🐠meeeyo.com", 144 | "ImageOverlayAlignment": "图像层叠加🐠meeeyo.com", 145 | "TextToImage": "文字图像🐠meeeyo.com", 146 | 147 | "ReadExcelData": "读取表格数据🐠meeeyo.com", 148 | "WriteExcelData": "写入表格数据🐠meeeyo.com", 149 | "WriteExcelImage": "图片插入表格🐠meeeyo.com", 150 | "FindExcelData": "查找表格数据🐠meeeyo.com", 151 | "ReadExcelRowOrColumnDiff": "读取表格数量差🐠meeeyo.com", 152 | 153 | #功能型节点:meyo_node_Functional 154 | "GetCurrentTime": "当前时间(戳)🐠meeeyo.com", 155 | "SimpleRandomSeed": "随机整数🐠meeeyo.com", 156 | "SelectionParameter": "选择参数🐠meeeyo.com", 157 | "ReadWebNode": "读取页面🐠meeeyo.com", 158 | "DecodePreview": "解码预览🐠meeeyo.com", 159 | } 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /fonts/simhei.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeeeyoAI/ComfyUI_StringOps/d683188ef829c14be8f1a05385345583bd82ee50/fonts/simhei.ttf -------------------------------------------------------------------------------- /fonts/simkai.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeeeyoAI/ComfyUI_StringOps/d683188ef829c14be8f1a05385345583bd82ee50/fonts/simkai.ttf -------------------------------------------------------------------------------- /image/meyo01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeeeyoAI/ComfyUI_StringOps/d683188ef829c14be8f1a05385345583bd82ee50/image/meyo01.png -------------------------------------------------------------------------------- /image/meyo02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeeeyoAI/ComfyUI_StringOps/d683188ef829c14be8f1a05385345583bd82ee50/image/meyo02.png -------------------------------------------------------------------------------- /image/meyo03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeeeyoAI/ComfyUI_StringOps/d683188ef829c14be8f1a05385345583bd82ee50/image/meyo03.png -------------------------------------------------------------------------------- /image/meyo04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeeeyoAI/ComfyUI_StringOps/d683188ef829c14be8f1a05385345583bd82ee50/image/meyo04.png -------------------------------------------------------------------------------- /image/meyo05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeeeyoAI/ComfyUI_StringOps/d683188ef829c14be8f1a05385345583bd82ee50/image/meyo05.png -------------------------------------------------------------------------------- /image/meyo06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeeeyoAI/ComfyUI_StringOps/d683188ef829c14be8f1a05385345583bd82ee50/image/meyo06.png -------------------------------------------------------------------------------- /image/v.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeeeyoAI/ComfyUI_StringOps/d683188ef829c14be8f1a05385345583bd82ee50/image/v.jpg -------------------------------------------------------------------------------- /meyo_node_Computational.py: -------------------------------------------------------------------------------- 1 | import math, random, torch 2 | import numpy as np 3 | from . import any_typ, note 4 | 5 | 6 | 7 | #======比较数值 8 | class CompareInt: 9 | @classmethod 10 | def INPUT_TYPES(cls): 11 | return { 12 | "required": { 13 | "input_float": ("FLOAT", {"default": 4.0}), 14 | "range": ("STRING", {"default": "3.5-5.5"}), 15 | }, 16 | "optional": {"any": (any_typ,)} 17 | } 18 | 19 | RETURN_TYPES = ("STRING",) 20 | FUNCTION = "compare_float_to_range" 21 | CATEGORY = "Meeeyo/Number" 22 | DESCRIPTION = note 23 | def IS_CHANGED(): return float("NaN") 24 | 25 | def compare_float_to_range(self, input_float, range, any=None): 26 | try: 27 | if '-' in range: 28 | lower_bound, upper_bound = map(float, range.split('-')) 29 | else: 30 | lower_bound = upper_bound = float(range) 31 | if input_float < lower_bound: 32 | return ("小",) 33 | elif input_float > upper_bound: 34 | return ("大",) 35 | else: 36 | return ("中",) 37 | except ValueError: 38 | return ("Error: Invalid input format.",) 39 | 40 | 41 | #======规范数值 42 | class FloatToInteger: 43 | @classmethod 44 | def INPUT_TYPES(cls): 45 | return { 46 | "required": { 47 | "float_value": ("FLOAT", {"default": 3.14}), 48 | "operation": (["四舍五入", "取大值", "取小值", "最近32倍"], {"default": "四舍五入"}), 49 | }, 50 | "optional": {"any": (any_typ,)} 51 | } 52 | 53 | RETURN_TYPES = ("INT",) 54 | FUNCTION = "convert_float_to_integer" 55 | CATEGORY = "Meeeyo/Number" 56 | DESCRIPTION = note 57 | def IS_CHANGED(): return float("NaN") 58 | 59 | def convert_float_to_integer(self, float_value, operation, any=None): 60 | if operation == "四舍五入": 61 | result = round(float_value) 62 | elif operation == "取大值": 63 | result = math.ceil(float_value) 64 | elif operation == "取小值": 65 | result = math.floor(float_value) 66 | elif operation == "最近32倍": 67 | result = round(float_value / 32) * 32 68 | return (result,) 69 | 70 | 71 | #======生成范围数组 72 | class GenerateNumbers: 73 | @classmethod 74 | def INPUT_TYPES(cls): 75 | return { 76 | "required": { 77 | "range_rule": ("STRING", {"default": "3|1-10"}), 78 | "mode": (["顺序", "随机"], {"default": "顺序"}), 79 | "prefix_suffix": ("STRING", {"default": "|"}), 80 | }, 81 | "optional": {"any": (any_typ,)} 82 | } 83 | 84 | RETURN_TYPES = ("STRING",) 85 | FUNCTION = "generate_numbers" 86 | CATEGORY = "Meeeyo/Number" 87 | DESCRIPTION = note 88 | def IS_CHANGED(): return float("NaN") 89 | 90 | def generate_numbers(self, range_rule, mode, prefix_suffix, any=None): 91 | try: 92 | start_str, range_str = range_rule.split('|') 93 | start = int(start_str) 94 | end_range = list(map(int, range_str.split('-'))) 95 | if len(end_range) == 1: 96 | end = end_range[0] 97 | numbers = [str(i).zfill(start) for i in range(1, end + 1)] 98 | else: 99 | start_range, end = end_range 100 | numbers = [str(i).zfill(start) for i in range(start_range, end + 1)] 101 | if prefix_suffix.strip(): 102 | prefix, suffix = prefix_suffix.split('|') 103 | else: 104 | prefix, suffix = "", "" 105 | if mode == "随机": 106 | random.shuffle(numbers) 107 | numbers = [f"{prefix}{num}{suffix}" for num in numbers] 108 | result = '\n'.join(numbers) 109 | return (result,) 110 | except ValueError: 111 | return ("",) 112 | 113 | 114 | #======范围内随机数 115 | class GetRandomIntegerInRange: 116 | @classmethod 117 | def INPUT_TYPES(cls): 118 | return { 119 | "required": { 120 | "range_str": ("STRING", {"default": "0-10"}), 121 | }, 122 | "optional": {"any": (any_typ,)} 123 | } 124 | 125 | RETURN_TYPES = ("INT", "STRING") 126 | FUNCTION = "get_random_integer_in_range" 127 | CATEGORY = "Meeeyo/Number" 128 | DESCRIPTION = note 129 | def IS_CHANGED(): return float("NaN") 130 | 131 | def get_random_integer_in_range(self, range_str, any=None): 132 | try: 133 | start, end = map(int, range_str.split('-')) 134 | if start > end: 135 | start, end = end, start 136 | random_int = random.randint(start, end) 137 | return (random_int, str(random_int)) 138 | except ValueError: 139 | return (0, "0") 140 | 141 | -------------------------------------------------------------------------------- /meyo_node_File.py: -------------------------------------------------------------------------------- 1 | import os, re, io, base64, csv, torch, shutil, requests, chardet, pathlib 2 | import openpyxl, folder_paths, node_helpers 3 | import numpy as np 4 | from PIL import Image, ImageOps, ImageSequence, ImageDraw, ImageFont 5 | from pathlib import Path 6 | from openpyxl.drawing.image import Image as OpenpyxlImage 7 | from PIL import Image as PILImage 8 | from io import BytesIO 9 | from . import any_typ, note 10 | 11 | 12 | 13 | #======全能加载图像 14 | class GenericImageLoader: 15 | @classmethod 16 | def INPUT_TYPES(cls): 17 | return { 18 | "required": { 19 | "image_input": ("STRING", {"default": ""}), 20 | } 21 | } 22 | RETURN_TYPES = ("IMAGE", "MASK") 23 | FUNCTION = "load_image" 24 | OUTPUT_NODE = False 25 | CATEGORY = "Meeeyo/File" 26 | DESCRIPTION = note 27 | def IS_CHANGED(): return float("NaN") 28 | 29 | def load_image(self, image_input): 30 | path = image_input.strip() 31 | for ctrl in ['\u202a', '\u202b', '\u202c', '\u202d', '\u202e']: 32 | while path.startswith(ctrl): 33 | path = path.lstrip(ctrl) 34 | 35 | source = None 36 | lower = path.lower() 37 | if lower.startswith('http://') or lower.startswith('https://'): 38 | source = 'network' 39 | elif os.path.isfile(path): 40 | source = 'local' 41 | else: 42 | source = 'base64' 43 | if source == 'local': 44 | img = Image.open(path) 45 | elif source == 'network': 46 | resp = requests.get(path) 47 | resp.raise_for_status() 48 | img = Image.open(io.BytesIO(resp.content)) 49 | else: # 'base64' 50 | if ',' in path and path.startswith('data:'): 51 | _, data = path.split(',', 1) 52 | else: 53 | data = path 54 | decoded = base64.b64decode(data) 55 | img = Image.open(io.BytesIO(decoded)) 56 | 57 | img = img.convert('RGBA') 58 | has_alpha = img.mode == 'RGBA' 59 | if has_alpha: 60 | alpha_channel = np.array(img)[:, :, 3] 61 | mask = (alpha_channel > 0).astype(np.float32) 62 | mask_tensor = torch.from_numpy(mask).unsqueeze(0).unsqueeze(0) 63 | else: 64 | mask_tensor = torch.zeros((1, 1, img.size[1], img.size[0]), dtype=torch.float32) 65 | np_img = np.array(img).astype(np.float32) / 255.0 66 | img_tensor = torch.from_numpy(np_img).unsqueeze(0) 67 | 68 | return img_tensor, mask_tensor 69 | 70 | 71 | #======加载重置图像 72 | class LoadAndAdjustImage: 73 | @classmethod 74 | def INPUT_TYPES(s): 75 | input_dir = folder_paths.get_input_directory() 76 | files = [f.name for f in Path(input_dir).iterdir() if f.is_file()] 77 | return { 78 | "required": { 79 | "image": (sorted(files), {"image_upload": True}), 80 | "max_dimension": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 8}), 81 | "size_option": (["No Change", "Custom", "Million Pixels", "Small", "Medium", "Large", 82 | "480P-H(vid 4:3)", "480P-V(vid 3:4)", "720P-H(vid 16:9)", "720P-V(vid 9:16)", "832×480", "480×832"], 83 | {"default": "No Change"}) 84 | } 85 | } 86 | 87 | RETURN_TYPES = ("IMAGE", "MASK", "STRING") 88 | RETURN_NAMES = ("image", "mask", "info") 89 | FUNCTION = "load_image" 90 | CATEGORY = "Meeeyo/File" 91 | DESCRIPTION = note 92 | def IS_CHANGED(): return float("NaN") 93 | 94 | def load_image(self, image, max_dimension, size_option): 95 | image_path = folder_paths.get_annotated_filepath(image) 96 | img = Image.open(image_path) 97 | W, H = img.size 98 | aspect_ratio = W / H 99 | 100 | def get_target_size(): 101 | if size_option == "No Change": 102 | # No resizing or cropping, just return the original size 103 | return W, H 104 | elif size_option == "Million Pixels": 105 | return self._resize_to_million_pixels(W, H) 106 | elif size_option == "Custom": 107 | ratio = min(max_dimension / W, max_dimension / H) 108 | return round(W * ratio), round(H * ratio) 109 | 110 | size_options = { 111 | "Small": ( 112 | (768, 512) if aspect_ratio >= 1.23 else 113 | (512, 768) if aspect_ratio <= 0.82 else 114 | (768, 768) 115 | ), 116 | "Medium": ( 117 | (1216, 832) if aspect_ratio >= 1.23 else 118 | (832, 1216) if aspect_ratio <= 0.82 else 119 | (1216, 1216) 120 | ), 121 | "Large": ( 122 | (1600, 1120) if aspect_ratio >= 1.23 else 123 | (1120, 1600) if aspect_ratio <= 0.82 else 124 | (1600, 1600) 125 | ), 126 | "Million Pixels": self._resize_to_million_pixels(W, H), # Million Pixels option 127 | "480P-H(vid 4:3)": (640, 480), # 480P-H, 640x480 128 | "480P-V(vid 3:4)": (480, 640), # 480P-V, 480x640 129 | "720P-H(vid 16:9)": (1280, 720), # 720P-H, 1280x720 130 | "720P-V(vid 9:16)": (720, 1280), # 720P-V, 720x1280 131 | "832×480": (832, 480), # 832x480 132 | "480×832": (480, 832), # 480x832 133 | } 134 | return size_options[size_option] 135 | 136 | target_width, target_height = get_target_size() 137 | output_images = [] 138 | output_masks = [] 139 | 140 | for frame in ImageSequence.Iterator(img): 141 | frame = ImageOps.exif_transpose(frame) 142 | if frame.mode == 'P': 143 | frame = frame.convert("RGBA") 144 | elif 'A' in frame.getbands(): 145 | frame = frame.convert("RGBA") 146 | 147 | if size_option == "No Change": 148 | # No resizing, just use the original frame 149 | image_frame = frame.convert("RGB") 150 | else: 151 | if size_option == "Custom" or size_option == "Million Pixels": 152 | ratio = min(target_width / W, target_height / H) 153 | adjusted_width = round(W * ratio) 154 | adjusted_height = round(H * ratio) 155 | image_frame = frame.convert("RGB").resize((adjusted_width, adjusted_height), Image.Resampling.BILINEAR) 156 | else: 157 | image_frame = frame.convert("RGB") 158 | image_frame = ImageOps.fit(image_frame, (target_width, target_height), method=Image.Resampling.BILINEAR, centering=(0.5, 0.5)) 159 | 160 | image_array = np.array(image_frame).astype(np.float32) / 255.0 161 | output_images.append(torch.from_numpy(image_array)[None,]) 162 | 163 | # Process the mask if available 164 | if 'A' in frame.getbands(): 165 | mask_frame = frame.getchannel('A') 166 | if size_option == "Custom" or size_option == "Million Pixels": 167 | mask_frame = mask_frame.resize((adjusted_width, adjusted_height), Image.Resampling.BILINEAR) 168 | else: 169 | mask_frame = ImageOps.fit(mask_frame, (target_width, target_height), method=Image.Resampling.BILINEAR, centering=(0.5, 0.5)) 170 | mask = np.array(mask_frame).astype(np.float32) / 255.0 171 | mask = 1. - mask 172 | else: 173 | if size_option == "Custom" or size_option == "Million Pixels": 174 | mask = np.zeros((adjusted_height, adjusted_width), dtype=np.float32) 175 | else: 176 | mask = np.zeros((target_height, target_width), dtype=np.float32) 177 | output_masks.append(torch.from_numpy(mask).unsqueeze(0)) 178 | 179 | output_image = torch.cat(output_images, dim=0) if len(output_images) > 1 else output_images[0] 180 | output_mask = torch.cat(output_masks, dim=0) if len(output_masks) > 1 else output_masks[0] 181 | info = f"Image Path: {image_path}\nOriginal Size: {W}x{H}\nAdjusted Size: {target_width}x{target_height}" 182 | return (output_image, output_mask, info) 183 | 184 | @classmethod 185 | def VALIDATE_INPUTS(s, image): 186 | if not folder_paths.exists_annotated_filepath(image): 187 | return f"Invalid image file: {image}" 188 | return True 189 | def _resize_to_million_pixels(self, W, H): 190 | aspect_ratio = W / H 191 | target_area = 1000000 # 1 million pixels 192 | if aspect_ratio > 1: # Landscape 193 | width = int(np.sqrt(target_area * aspect_ratio)) 194 | height = int(target_area / width) 195 | else: # Portrait 196 | height = int(np.sqrt(target_area / aspect_ratio)) 197 | width = int(target_area / height) 198 | width = (width + 7) // 8 * 8 199 | height = (height + 7) // 8 * 8 200 | return width, height 201 | 202 | 203 | #======重置图像 204 | class ImageAdjuster: 205 | @classmethod 206 | def INPUT_TYPES(s): 207 | return { 208 | "required": { 209 | "image": ("IMAGE",), 210 | "mask": ("MASK",), # 添加遮罩输入 211 | "max_dimension": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 8}), 212 | "size_option": ([ 213 | "Custom", "Million Pixels", "Small", "Medium", "Large", 214 | "480P-H(vid 4:3)", "480P-V(vid 3:4)", "720P-H(vid 16:9)", "720P-V(vid 9:16)", "832×480", "480×832"], 215 | {"default": "Million Pixels"} 216 | ) 217 | } 218 | } 219 | 220 | RETURN_TYPES = ("IMAGE", "MASK", "INT", "INT") 221 | RETURN_NAMES = ("image", "mask", "width", "height") 222 | FUNCTION = "process_image" 223 | CATEGORY = "Meeeyo/File" 224 | DESCRIPTION = note 225 | def IS_CHANGED(): return float("NaN") 226 | 227 | def process_image(self, image, mask, max_dimension=1024, size_option="Custom"): 228 | batch_size = image.shape[0] 229 | processed_images = [] 230 | processed_masks = [] 231 | widths = [] 232 | heights = [] 233 | 234 | for i in range(batch_size): 235 | current_image = image[i] 236 | current_mask = mask[i] 237 | 238 | input_pil_image = Image.fromarray((current_image.numpy() * 255).astype(np.uint8)) 239 | input_pil_mask = Image.fromarray((current_mask.numpy() * 255).astype(np.uint8)) 240 | 241 | W, H = input_pil_image.size 242 | 243 | processed_image_pil = input_pil_image.copy() 244 | processed_image_pil = ImageOps.exif_transpose(processed_image_pil) 245 | 246 | processed_mask_pil = input_pil_mask.copy() 247 | processed_mask_pil = ImageOps.exif_transpose(processed_mask_pil) 248 | 249 | if processed_image_pil.mode == 'P': 250 | processed_image_pil = processed_image_pil.convert("RGBA") 251 | elif 'A' in processed_image_pil.getbands(): 252 | processed_image_pil = processed_image_pil.convert("RGBA") 253 | 254 | if processed_mask_pil.mode != "L": 255 | processed_mask_pil = processed_mask_pil.convert("L") 256 | 257 | if size_option != "Custom": 258 | aspect_ratio = W / H 259 | 260 | size_options = { 261 | "Small": ( 262 | (768, 512) if aspect_ratio >= 1.23 else 263 | (512, 768) if aspect_ratio <= 0.82 else 264 | (768, 768) 265 | ), 266 | "Medium": ( 267 | (1216, 832) if aspect_ratio >= 1.23 else 268 | (832, 1216) if aspect_ratio <= 0.82 else 269 | (1216, 1216) 270 | ), 271 | "Large": ( 272 | (1600, 1120) if aspect_ratio >= 1.23 else 273 | (1120, 1600) if aspect_ratio <= 0.82 else 274 | (1600, 1600) 275 | ), 276 | "Million Pixels": self._resize_to_million_pixels(W, H), 277 | "480P-H(vid 4:3)": (640, 480), 278 | "480P-V(vid 3:4)": (480, 640), 279 | "720P-H(vid 16:9)": (1280, 720), 280 | "720P-V(vid 9:16)": (720, 1280), 281 | "832×480": (832, 480), 282 | "480×832": (480, 832) 283 | } 284 | 285 | target_width, target_height = size_options[size_option] 286 | processed_image_pil = processed_image_pil.convert("RGB") 287 | processed_image_pil = ImageOps.fit(processed_image_pil, (target_width, target_height), method=Image.Resampling.BILINEAR, centering=(0.5, 0.5)) 288 | 289 | processed_mask_pil = ImageOps.fit(processed_mask_pil, (target_width, target_height), method=Image.Resampling.BILINEAR, centering=(0.5, 0.5)) 290 | else: 291 | ratio = min(max_dimension / W, max_dimension / H) 292 | adjusted_width = round(W * ratio) 293 | adjusted_height = round(H * ratio) 294 | 295 | processed_image_pil = processed_image_pil.convert("RGB") 296 | processed_image_pil = processed_image_pil.resize((adjusted_width, adjusted_height), Image.Resampling.BILINEAR) 297 | 298 | processed_mask_pil = processed_mask_pil.resize((adjusted_width, adjusted_height), Image.Resampling.BILINEAR) 299 | 300 | processed_image = np.array(processed_image_pil).astype(np.float32) / 255.0 301 | processed_image = torch.from_numpy(processed_image) 302 | processed_images.append(processed_image) 303 | 304 | processed_mask = np.array(processed_mask_pil).astype(np.float32) / 255.0 305 | processed_mask = torch.from_numpy(processed_mask) 306 | processed_masks.append(processed_mask) 307 | 308 | if size_option != "Custom": 309 | widths.append(target_width) 310 | heights.append(target_height) 311 | else: 312 | widths.append(adjusted_width) 313 | heights.append(adjusted_height) 314 | 315 | output_image = torch.stack(processed_images) 316 | output_mask = torch.stack(processed_masks) 317 | 318 | if all(w == widths[0] for w in widths) and all(h == heights[0] for h in heights): 319 | return (output_image, output_mask, widths[0], heights[0]) 320 | else: 321 | return (output_image, output_mask, widths[0], heights[0]) 322 | 323 | def _resize_to_million_pixels(self, W, H): 324 | aspect_ratio = W / H 325 | target_area = 1000000 326 | 327 | if aspect_ratio > 1: 328 | width = int(np.sqrt(target_area * aspect_ratio)) 329 | height = int(target_area / width) 330 | else: 331 | height = int(np.sqrt(target_area / aspect_ratio)) 332 | width = int(target_area / height) 333 | 334 | width = (width + 7) // 8 * 8 335 | height = (height + 7) // 8 * 8 336 | 337 | return width, height 338 | 339 | 340 | #======裁剪图像 341 | class CustomCrop: 342 | @classmethod 343 | def INPUT_TYPES(cls): 344 | return { 345 | "required": { 346 | "image": ("IMAGE",), 347 | "mask": ("MASK",), # 新增遮罩输入 348 | "width": ("INT", {"default": 768, "min": 0, "max": 4096, "step": 8}), 349 | "height": ("INT", {"default": 768, "min": 0, "max": 4096, "step": 8}), 350 | } 351 | } 352 | 353 | RETURN_TYPES = ("IMAGE", "MASK", "INT", "INT") # 新增遮罩输出 354 | RETURN_NAMES = ("image", "mask", "width", "height") 355 | FUNCTION = "process_image" 356 | CATEGORY = "Meeeyo/File" 357 | DESCRIPTION = note 358 | def IS_CHANGED(): return float("NaN") 359 | 360 | def process_image(self, image, mask, width=768, height=768): 361 | input_image = Image.fromarray((image.squeeze(0).numpy() * 255).astype(np.uint8)) 362 | input_mask = Image.fromarray((mask.squeeze(0).numpy() * 255).astype(np.uint8)) # 转换遮罩为PIL图像 363 | 364 | W, H = input_image.size 365 | 366 | processed_images = [] 367 | processed_masks = [] # 新增遮罩处理列表 368 | 369 | for frame in [input_image]: 370 | frame = ImageOps.exif_transpose(frame) 371 | 372 | if frame.mode == 'P': 373 | frame = frame.convert("RGBA") 374 | elif 'A' in frame.getbands(): 375 | frame = frame.convert("RGBA") 376 | 377 | processed_image = frame.convert("RGB") 378 | processed_image = ImageOps.fit(processed_image, (width, height), method=Image.Resampling.BILINEAR, centering=(0.5, 0.5)) 379 | 380 | processed_image = np.array(processed_image).astype(np.float32) / 255.0 381 | processed_image = torch.from_numpy(processed_image)[None,] 382 | processed_images.append(processed_image) 383 | 384 | # 处理遮罩 385 | input_mask = ImageOps.exif_transpose(input_mask) 386 | processed_mask = input_mask.convert("L") # 确保遮罩是灰度图像 387 | processed_mask = ImageOps.fit(processed_mask, (width, height), method=Image.Resampling.BILINEAR, centering=(0.5, 0.5)) 388 | processed_mask = np.array(processed_mask).astype(np.float32) / 255.0 389 | processed_mask = torch.from_numpy(processed_mask)[None,] 390 | processed_masks.append(processed_mask) 391 | 392 | output_image = torch.cat(processed_images, dim=0) if len(processed_images) > 1 else processed_images[0] 393 | output_mask = torch.cat(processed_masks, dim=0) if len(processed_masks) > 1 else processed_masks[0] 394 | 395 | return (output_image, output_mask, width, height) 396 | 397 | 398 | #======保存图像 399 | class SaveImagEX: 400 | def __init__(self): 401 | self.output_dir = folder_paths.get_output_directory() 402 | 403 | @classmethod 404 | def INPUT_TYPES(cls): 405 | return { 406 | "required": { 407 | "image": ("IMAGE",), 408 | "save_path": ("STRING", {"default": "./output"}), 409 | "image_name": ("STRING", {"default": "ComfyUI"}), 410 | "image_format": (["PNG", "JPG"], {"default": "JPG"}) 411 | }, 412 | } 413 | 414 | RETURN_TYPES = ("IMAGE",) 415 | RETURN_NAMES = ("image",) 416 | FUNCTION = "save_image" 417 | OUTPUT_NODE = True 418 | CATEGORY = "Meeeyo/File" 419 | DESCRIPTION = note 420 | def IS_CHANGED(): return float("NaN") 421 | 422 | def save_image(self, image, save_path, image_name, image_format): 423 | if not isinstance(image, torch.Tensor): 424 | raise ValueError("Invalid image tensor format") 425 | if save_path == "./output": 426 | save_path = self.output_dir 427 | elif not os.path.isabs(save_path): 428 | save_path = os.path.join(self.output_dir, save_path) 429 | os.makedirs(save_path, exist_ok=True) 430 | 431 | # 移除可能存在的扩展名,然后添加用户选择的格式对应的扩展名 432 | base_name = os.path.splitext(image_name)[0] 433 | 434 | batch_size = image.shape[0] 435 | channel_to_mode = {1: "L", 3: "RGB", 4: "RGBA"} 436 | 437 | for i in range(batch_size): 438 | if image_format == "PNG": 439 | filename = f"{base_name}.png" if batch_size == 1 else f"{base_name}_{i:05d}.png" 440 | save_format = "PNG" 441 | save_params = {"compress_level": 0} 442 | else: # JPG 443 | filename = f"{base_name}.jpg" if batch_size == 1 else f"{base_name}_{i:05d}.jpg" 444 | save_format = "JPEG" 445 | save_params = {"quality": 100} 446 | 447 | full_path = os.path.join(save_path, filename) 448 | single_image = image[i].cpu().numpy() 449 | single_image = (single_image * 255).astype('uint8') 450 | channels = single_image.shape[2] 451 | if channels not in channel_to_mode: 452 | raise ValueError(f"Unsupported channel number: {channels}") 453 | mode = channel_to_mode[channels] 454 | if channels == 1: 455 | single_image = single_image[:, :, 0] 456 | pil_image = Image.fromarray(single_image, mode) 457 | 458 | if image_format == "JPG": 459 | pil_image = pil_image.convert("RGB") 460 | 461 | pil_image.save(full_path, format=save_format, **save_params) 462 | return (image,) 463 | 464 | 465 | #======文件操作 466 | class FileCopyCutNode: 467 | @classmethod 468 | def INPUT_TYPES(cls): 469 | return { 470 | "required": { 471 | "source_path": ("STRING", {"default": "", "multiline": False}), 472 | "destination_path": ("STRING", {"default": "", "multiline": False}), 473 | "operation": (["copy", "cut"], {"default": "copy"}), 474 | }, 475 | "optional": {"any": (any_typ,)} # 可以进一步简化成一行 476 | } 477 | 478 | RETURN_TYPES = ("STRING",) # 返回操作结果字符串 479 | RETURN_NAMES = ("result",) 480 | FUNCTION = "copy_cut_file" 481 | CATEGORY = "Meeeyo/File" 482 | DESCRIPTION = note 483 | def IS_CHANGED(): return float("NaN") 484 | 485 | def copy_cut_file(self, source_path, destination_path, operation, any=None): 486 | result = "执行失败" 487 | try: 488 | if not os.path.isfile(source_path): 489 | raise FileNotFoundError(f"源文件未找到: {source_path}") 490 | 491 | os.makedirs(os.path.dirname(destination_path), exist_ok=True) 492 | 493 | if operation == "copy": 494 | shutil.copy2(source_path, destination_path) 495 | result = "执行成功: 文件已复制" 496 | elif operation == "cut": 497 | shutil.move(source_path, destination_path) 498 | result = "执行成功: 文件已剪切" 499 | else: 500 | raise ValueError("操作类型无效,仅支持 'copy' 或 'cut'。") 501 | except FileNotFoundError as e: 502 | result = f"执行失败: {str(e)}" 503 | except ValueError as e: 504 | result = f"执行失败: {str(e)}" 505 | except Exception as e: 506 | result = f"执行失败: {str(e)}" 507 | 508 | return (result,) 509 | 510 | 511 | #======替换文件名 512 | class FileNameReplacer: 513 | @classmethod 514 | def INPUT_TYPES(cls): 515 | return { 516 | "required": { 517 | "file_path": ("STRING", {"default": "path/to/your/file.jpg"}), 518 | "new_file_name": ("STRING", {"default": ""}), 519 | }, 520 | "optional": {"any": (any_typ,)} 521 | } 522 | 523 | RETURN_TYPES = ("STRING",) 524 | FUNCTION = "replace_file_name" 525 | CATEGORY = "Meeeyo/File" 526 | DESCRIPTION = note 527 | def IS_CHANGED(): return float("NaN") 528 | 529 | def replace_file_name(self, file_path, new_file_name, any=None): 530 | dir_name = os.path.dirname(file_path) 531 | _, file_ext = os.path.splitext(file_path) 532 | new_file_name = self.sanitize_file_name(new_file_name) 533 | new_file_path = os.path.join(dir_name, new_file_name + file_ext) 534 | return (new_file_path,) 535 | def sanitize_file_name(self, file_name): 536 | invalid_chars = r'[\/:*?"<>|]' 537 | return re.sub(invalid_chars, '_', file_name) 538 | 539 | 540 | #======文本写入TXT 541 | class WriteToTxtFile: 542 | @classmethod 543 | def INPUT_TYPES(cls): 544 | return { 545 | "required": { 546 | "text_content": ("STRING", {"default": "", "multiline": True}), 547 | "file_path": ("STRING", {"default": "path/to/your/file.txt"}), 548 | }, 549 | "optional": {"any": (any_typ,)} 550 | } 551 | 552 | RETURN_TYPES = ("STRING",) 553 | FUNCTION = "write_to_txt" 554 | CATEGORY = "Meeeyo/File" 555 | DESCRIPTION = note 556 | def IS_CHANGED(): return float("NaN") 557 | 558 | def write_to_txt(self, text_content, file_path, any=None): 559 | try: 560 | dir_path = os.path.dirname(file_path) 561 | if not os.path.exists(dir_path): 562 | os.makedirs(dir_path) 563 | file_exists = os.path.exists(file_path) 564 | mode = 'a' if file_exists else 'w' 565 | 566 | with open(file_path, mode, encoding='utf-8') as f: 567 | if file_exists: 568 | f.write('\n\n') 569 | f.write(text_content) 570 | return ("Write successful: " + text_content,) 571 | except Exception as e: 572 | return (f"Error: {str(e)}",) 573 | 574 | 575 | #======清理文件 576 | class FileDeleteNode: 577 | @classmethod 578 | def INPUT_TYPES(cls): 579 | return { 580 | "required": { 581 | "items_to_delete": ("STRING", {"default": "33.png\ncs1/01.png\ncs1", "multiline": True}), 582 | }, 583 | "optional": {"any": (any_typ,)} 584 | } 585 | 586 | RETURN_TYPES = ("STRING",) 587 | RETURN_NAMES = ("result",) 588 | FUNCTION = "delete_files" 589 | CATEGORY = "Meeeyo/File" 590 | DESCRIPTION = note 591 | def IS_CHANGED(): return float("NaN") 592 | 593 | def delete_files(self, items_to_delete, any=None): 594 | result = "执行成功: 所有指定项已从output目录删除" 595 | error_messages = [] 596 | base_output_dir = Path.cwd() / COMFYUI_OUTPUT_DIR 597 | items = items_to_delete.strip().split('\n') 598 | 599 | for item in items: 600 | item = item.strip() 601 | if not item: 602 | continue 603 | if item == "[DeleteAll]": 604 | try: 605 | for file_or_dir in base_output_dir.glob('*'): 606 | if file_or_dir.is_file() or file_or_dir.is_symlink(): 607 | file_or_dir.unlink() 608 | elif file_or_dir.is_dir(): 609 | shutil.rmtree(file_or_dir) 610 | continue 611 | except Exception as e: 612 | error_messages.append(f"从output目录删除全部失败: {str(e)}") 613 | continue 614 | target_path = base_output_dir / item 615 | try: 616 | target_path.relative_to(base_output_dir) 617 | except ValueError: 618 | error_messages.append(f"{item} 不在output目录范围内,无法删除") 619 | continue 620 | if not target_path.exists(): 621 | error_messages.append(f"在output目录下找不到 {item}") 622 | continue 623 | try: 624 | if target_path.is_file() or target_path.is_symlink(): 625 | target_path.unlink() 626 | elif target_path.is_dir(): 627 | shutil.rmtree(target_path) 628 | except Exception as e: 629 | error_messages.append(f"从output目录删除 {item} 失败: {str(e)}") 630 | if error_messages: 631 | result = "部分执行失败:\n" + "\n".join(error_messages) 632 | return (result,) 633 | 634 | 635 | #======文件路径和后缀统计 636 | class FileListAndSuffix: 637 | @classmethod 638 | def INPUT_TYPES(cls): 639 | return { 640 | "required": { 641 | "folder_path": ("STRING",), 642 | "file_extension": (["jpg", "png", "jpg&png", "txt", "csv", "all"], {"default": "jpg"}), 643 | }, 644 | "optional": {"any": (any_typ,)} 645 | } 646 | 647 | RETURN_TYPES = ("STRING", "INT", "LIST") 648 | FUNCTION = "file_list_and_suffix" 649 | CATEGORY = "Meeeyo/File" 650 | DESCRIPTION = note 651 | def IS_CHANGED(): return float("NaN") 652 | 653 | def file_list_and_suffix(self, folder_path, file_extension, any=None): 654 | try: 655 | if not os.path.isdir(folder_path): 656 | return ("", 0, []) 657 | 658 | if file_extension == "all": 659 | file_paths = [os.path.join(folder_path, f) for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f))] 660 | elif file_extension == "jpg&png": 661 | file_paths = [os.path.join(folder_path, f) for f in os.listdir(folder_path) if f.lower().endswith(('.jpg', '.png'))] 662 | else: 663 | file_paths = [os.path.join(folder_path, f) for f in os.listdir(folder_path) if f.lower().endswith('.' + file_extension)] 664 | 665 | return ('\n'.join(file_paths), len(file_paths), file_paths) 666 | except Exception as e: 667 | return ("", 0, []) 668 | 669 | 670 | #======文字图像 671 | class TextToImage: 672 | @classmethod 673 | def INPUT_TYPES(cls): 674 | fonts_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fonts") 675 | if not os.path.exists(fonts_dir): 676 | os.makedirs(fonts_dir) 677 | font_files = [] 678 | else: 679 | font_files = [file for file in os.listdir(fonts_dir) if file.lower().endswith(('.ttf', '.otf'))] 680 | font_files = font_files or ["arial.ttf"] 681 | return { 682 | "required": { 683 | "text": ("STRING", {"default": "Hello, World!", "multiline": True}), 684 | "font": (font_files, ), 685 | "max_width": ("INT", {"default": 300, "min": 1, "max": 2048, "step": 1}), 686 | "font_properties": ("STRING", {"default": "#FFFFFF,1,1,C,1", "multiline": False}), 687 | "font_stroke": ("STRING", {"default": "#000000,2,1", "multiline": False}), 688 | "font_background": ("STRING", {"default": "#333333,5,10,1", "multiline": False}) 689 | }, 690 | "optional": {"any": (any_typ,)} 691 | } 692 | RETURN_TYPES = ("IMAGE",) 693 | FUNCTION = "generate_text_image" 694 | CATEGORY = "Meeeyo/File" 695 | DESCRIPTION = note 696 | def IS_CHANGED(): return float("NaN") 697 | 698 | def generate_text_image(self, text, font, max_width, font_properties, font_stroke, font_background): 699 | fonts_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fonts") 700 | font_path = os.path.join(fonts_dir, font) 701 | try: 702 | font = ImageFont.truetype(font_path, 1) 703 | except Exception as e: 704 | return None 705 | 706 | draw = ImageDraw.Draw(Image.new('RGBA', (1, 1))) 707 | lines = text.split("\n") 708 | max_text_width = 0 709 | text_height = 0 710 | for line in lines: 711 | left, top, right, bottom = draw.textbbox((0, 0), line, font=font) 712 | line_width = right - left 713 | line_height = bottom - top 714 | max_text_width = max(max_text_width, line_width) 715 | text_height += line_height 716 | 717 | if max_text_width == 0: 718 | max_text_width = 1 719 | 720 | ratio = max_width / max_text_width 721 | new_font_size = int(1 * ratio) 722 | font = ImageFont.truetype(font_path, new_font_size) 723 | 724 | font_color = "#FFFFFF" 725 | letter_spacing_factor = 1.0 726 | line_spacing_factor = 1.0 727 | alignment = "C" 728 | opacity = 1.0 729 | stroke_color = "#000000" 730 | stroke_width = 0.0 731 | stroke_opacity = 1.0 732 | background_color = "#333333" 733 | expand_width = 5 734 | corner_radius = 10 735 | background_opacity = 0.9 736 | 737 | try: 738 | if font_properties.strip() == "": 739 | font_color = "#FFFFFF" 740 | letter_spacing_factor = 1.0 741 | line_spacing_factor = 1.0 742 | alignment = "C" 743 | opacity = 1.0 744 | else: 745 | props = font_properties.split(',') 746 | if len(props) >= 5: 747 | font_color = props[0].strip() 748 | letter_spacing_factor = float(props[1]) if props[1] else 1.0 749 | line_spacing_factor = float(props[2]) if props[2] else 1.0 750 | alignment = props[3].strip().upper() 751 | opacity = float(props[4]) if props[4] else 1.0 752 | else: 753 | font_color = "#FFFFFF" 754 | letter_spacing_factor = 1.0 755 | line_spacing_factor = 1.0 756 | alignment = "C" 757 | opacity = 1.0 758 | except: 759 | font_color = "#FFFFFF" 760 | letter_spacing_factor = 1.0 761 | line_spacing_factor = 1.0 762 | alignment = "C" 763 | opacity = 1.0 764 | 765 | try: 766 | if font_stroke.strip() == "": 767 | stroke_width = 0.0 768 | else: 769 | stroke_props = font_stroke.split(',') 770 | if len(stroke_props) >= 3: 771 | stroke_color = stroke_props[0].strip() 772 | stroke_width = float(stroke_props[1]) if stroke_props[1] else 1.0 773 | stroke_opacity = float(stroke_props[2]) if stroke_props[2] else 1.0 774 | else: 775 | stroke_width = 0.0 776 | except: 777 | stroke_width = 0.0 778 | 779 | try: 780 | if font_background.strip() == "": 781 | background_color = None 782 | else: 783 | bg_props = font_background.split(',') 784 | if len(bg_props) >= 4: 785 | background_color = bg_props[0].strip() 786 | expand_width = int(bg_props[1]) if bg_props[1] else 5 787 | corner_radius = int(bg_props[2]) if bg_props[2] else 10 788 | background_opacity = float(bg_props[3]) if bg_props[3] else 0.9 789 | else: 790 | background_color = None 791 | except: 792 | background_color = None 793 | 794 | actual_max_width = 0 795 | for line in lines: 796 | line_width = 0 797 | for char in line: 798 | char_width = draw.textbbox((0, 0), char, font=font)[2] 799 | line_width += char_width + (font.size * (letter_spacing_factor - 1)) 800 | actual_max_width = max(actual_max_width, line_width) 801 | 802 | if actual_max_width > max_width: 803 | ratio = max_width / actual_max_width 804 | new_font_size = int(new_font_size * ratio) 805 | font = ImageFont.truetype(font_path, new_font_size) 806 | 807 | font_ascent, font_descent = font.getmetrics() 808 | line_height = font_ascent + font_descent 809 | 810 | if len(lines) > 1: 811 | text_height = line_height * (len(lines) - 1) * line_spacing_factor + line_height 812 | else: 813 | text_height = line_height 814 | 815 | image_width = max_width 816 | image_height = int(text_height + new_font_size * 0.2) 817 | image = Image.new('RGBA', (image_width, image_height), (0, 0, 0, 0)) 818 | draw = ImageDraw.Draw(image) 819 | 820 | if background_color is not None: 821 | try: 822 | background_color_tuple = ( 823 | int(background_color[1:3], 16), 824 | int(background_color[3:5], 16), 825 | int(background_color[5:7], 16), 826 | int(background_opacity * 255) 827 | ) 828 | draw.rounded_rectangle( 829 | [0, 0, image_width, image_height], 830 | fill=background_color_tuple, 831 | radius=corner_radius 832 | ) 833 | except: 834 | pass 835 | 836 | y_text = new_font_size * 0.1 837 | try: 838 | font_color_tuple = ( 839 | int(font_color[1:3], 16), 840 | int(font_color[3:5], 16), 841 | int(font_color[5:7], 16), 842 | int(opacity * 255) 843 | ) 844 | stroke_color_tuple = ( 845 | int(stroke_color[1:3], 16), 846 | int(stroke_color[3:5], 16), 847 | int(stroke_color[5:7], 16), 848 | int(stroke_opacity * 255) 849 | ) 850 | except: 851 | font_color_tuple = (255, 255, 255, 255) 852 | stroke_color_tuple = (0, 0, 0, 255) 853 | 854 | for i, line in enumerate(lines): 855 | line_width = 0 856 | for char in line: 857 | char_width = draw.textbbox((0, 0), char, font=font)[2] 858 | line_width += char_width + (font.size * (letter_spacing_factor - 1)) 859 | line_width = max(line_width, 1) 860 | 861 | if alignment == "L": 862 | x = 0 863 | elif alignment == "R": 864 | x = max_width - line_width 865 | else: 866 | x = (max_width - line_width) / 2 867 | 868 | if stroke_width > 0: 869 | for sx in range(-int(stroke_width), int(stroke_width) + 1): 870 | for sy in range(-int(stroke_width), int(stroke_width) + 1): 871 | if sx == 0 and sy == 0: 872 | continue 873 | char_x = x + sx 874 | char_y = y_text + sy 875 | for char in line: 876 | char_width = draw.textbbox((0, 0), char, font=font)[2] 877 | draw.text((char_x, char_y), char, font=font, fill=stroke_color_tuple) 878 | char_x += char_width + (font.size * (letter_spacing_factor - 1)) 879 | 880 | char_x = x 881 | for char in line: 882 | char_width = draw.textbbox((0, 0), char, font=font)[2] 883 | draw.text((char_x, y_text), char, font=font, fill=font_color_tuple) 884 | char_x += char_width + (font.size * (letter_spacing_factor - 1)) 885 | 886 | if i < len(lines) - 1: 887 | y_text += line_height * line_spacing_factor 888 | else: 889 | y_text += line_height 890 | 891 | image_data = np.array(image) 892 | alpha_channel = image_data[:, :, 3] 893 | non_zero_indices = np.where(alpha_channel > 0) 894 | if len(non_zero_indices[0]) > 0: 895 | min_y = np.min(non_zero_indices[0]) 896 | max_y = np.max(non_zero_indices[0]) 897 | min_x = np.min(non_zero_indices[1]) 898 | max_x = np.max(non_zero_indices[1]) 899 | image = image.crop((min_x, min_y, max_x + 1, max_y + 1)) 900 | else: 901 | pass 902 | 903 | text_content_width = max_x - min_x + 1 if len(non_zero_indices[0]) > 0 else max_width 904 | 905 | image_width, image_height = image.size 906 | if text_content_width < max_width: 907 | new_image = Image.new('RGBA', (max_width, image_height), (0, 0, 0, 0)) 908 | new_draw = ImageDraw.Draw(new_image) 909 | x_offset = (max_width - text_content_width) // 2 910 | new_image.paste(image, (x_offset, 0)) 911 | image = new_image 912 | 913 | image_width, image_height = image.size 914 | if image_width > max_width: 915 | height_ratio = image_height / image_width 916 | image = image.resize((max_width, int(max_width * height_ratio))) 917 | 918 | image_np = np.array(image).astype(np.float32) / 255.0 919 | image_tensor = torch.from_numpy(image_np).unsqueeze(0) 920 | 921 | return (image_tensor,) 922 | 923 | 924 | #======图像层叠加 925 | class ImageOverlayAlignment: 926 | @classmethod 927 | def INPUT_TYPES(cls): 928 | return { 929 | "required": { 930 | "image1": ("IMAGE", {"forceInput": True}), 931 | "image2": ("IMAGE", {"forceInput": True}), 932 | "alignment": (["top_left", "top_center", "top_right", "bottom_left", "bottom_center", "bottom_right", "center"], ), 933 | "scale": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1}), 934 | "opacity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.1}), 935 | "offset": ("STRING", {"default": "0,0,0,0", "multiline": False}) 936 | } 937 | } 938 | 939 | RETURN_TYPES = ("IMAGE",) 940 | FUNCTION = "overlay_images" 941 | CATEGORY = "Meeeyo/File" 942 | DESCRIPTION = note 943 | def IS_CHANGED(): return float("NaN") 944 | 945 | def overlay_images(self, image1, image2, alignment, offset, scale, opacity): 946 | image1_np = image1.cpu().numpy().squeeze() 947 | image2_np = image2.cpu().numpy().squeeze() 948 | img1 = Image.fromarray((image1_np * 255).astype(np.uint8)).convert('RGBA') 949 | img2 = Image.fromarray((image2_np * 255).astype(np.uint8)).convert('RGBA') 950 | img1_width, img1_height = img1.size 951 | new_width = int(img1_width * scale) 952 | new_height = int(img1_height * scale) 953 | img1 = img1.resize((new_width, new_height), Image.LANCZOS) 954 | img1 = self.adjust_opacity(img1, opacity) 955 | img1_width, img1_height = img1.size 956 | img2_width, img2_height = img2.size 957 | max_width = max(img1_width, img2_width) 958 | max_height = max(img1_height, img2_height) 959 | canvas = Image.new('RGBA', (max_width, max_height), (0, 0, 0, 0)) 960 | if alignment == "top_left": 961 | img2_x, img2_y = 0, 0 962 | elif alignment == "top_center": 963 | img2_x = (max_width - img2_width) // 2 964 | img2_y = 0 965 | elif alignment == "top_right": 966 | img2_x = max_width - img2_width 967 | img2_y = 0 968 | elif alignment == "bottom_left": 969 | img2_x = 0 970 | img2_y = max_height - img2_height 971 | elif alignment == "bottom_center": 972 | img2_x = (max_width - img2_width) // 2 973 | img2_y = max_height - img2_height 974 | elif alignment == "bottom_right": 975 | img2_x = max_width - img2_width 976 | img2_y = max_height - img2_height 977 | elif alignment == "center": 978 | img2_x = (max_width - img2_width) // 2 979 | img2_y = (max_height - img2_height) // 2 980 | 981 | right_offset, left_offset, down_offset, up_offset = 0, 0, 0, 0 982 | offset_list = offset.split(',') 983 | if len(offset_list) >= 4: 984 | try: 985 | right_offset = int(offset_list[0]) if offset_list[0] else 0 986 | left_offset = int(offset_list[1]) if offset_list[1] else 0 987 | down_offset = int(offset_list[2]) if offset_list[2] else 0 988 | up_offset = int(offset_list[3]) if offset_list[3] else 0 989 | except ValueError: 990 | pass 991 | if alignment == "top_left": 992 | img1_x, img1_y = 0, 0 993 | elif alignment == "top_center": 994 | img1_x = (max_width - img1_width) // 2 995 | img1_y = 0 996 | elif alignment == "top_right": 997 | img1_x = max_width - img1_width 998 | img1_y = 0 999 | elif alignment == "bottom_left": 1000 | img1_x = 0 1001 | img1_y = max_height - img1_height 1002 | elif alignment == "bottom_center": 1003 | img1_x = (max_width - img1_width) // 2 1004 | img1_y = max_height - img1_height 1005 | elif alignment == "bottom_right": 1006 | img1_x = max_width - img1_width 1007 | img1_y = max_height - img1_height 1008 | elif alignment == "center": 1009 | img1_x = (max_width - img1_width) // 2 1010 | img1_y = (max_height - img1_height) // 2 1011 | img1_x += right_offset - left_offset 1012 | img1_y += down_offset - up_offset 1013 | img1_x = max(0, min(img1_x, max_width - img1_width)) 1014 | img1_y = max(0, min(img1_y, max_height - img1_height)) 1015 | img2_x = max(0, min(img2_x, max_width - img2_width)) 1016 | img2_y = max(0, min(img2_y, max_height - img2_height)) 1017 | canvas.paste(img2, (img2_x, img2_y), img2.split()[-1]) 1018 | canvas.paste(img1, (img1_x, img1_y), img1.split()[-1]) 1019 | output_image = np.array(canvas).astype(np.float32) / 255.0 1020 | output_tensor = torch.from_numpy(output_image).unsqueeze(0) 1021 | return (output_tensor,) 1022 | 1023 | def adjust_opacity(self, img, opacity): 1024 | if opacity < 1.0: 1025 | img = img.copy() 1026 | alpha = np.array(img.split()[-1]) * opacity 1027 | alpha = alpha.astype(np.uint8) 1028 | img.putalpha(Image.fromarray(alpha)) 1029 | return img 1030 | 1031 | 1032 | #======读取表格数据 1033 | class ReadExcelData: 1034 | @classmethod 1035 | def INPUT_TYPES(cls): 1036 | return { 1037 | "required": { 1038 | "excel_path": ("STRING", {"default": "path/to/your/file.xlsx"}), 1039 | "sheet_name": ("STRING", {"default": "Sheet1"}), 1040 | "row_range": ("STRING", {"default": "2-3"}), 1041 | "col_range": ("STRING", {"default": "1-4"}), 1042 | }, 1043 | "optional": {"any": (any_typ,)} 1044 | } 1045 | 1046 | RETURN_TYPES = ("STRING",) 1047 | FUNCTION = "read_excel_data" 1048 | CATEGORY = "Meeeyo/File" 1049 | DESCRIPTION = note 1050 | def IS_CHANGED(): return float("NaN") 1051 | 1052 | def read_excel_data(self, excel_path, sheet_name, row_range, col_range, any=None): 1053 | try: 1054 | if "-" in row_range: 1055 | start_row, end_row = map(int, row_range.split("-")) 1056 | else: 1057 | start_row = end_row = int(row_range) 1058 | 1059 | if "-" in col_range: 1060 | start_col, end_col = map(int, col_range.split("-")) 1061 | else: 1062 | start_col = end_col = int(col_range) 1063 | 1064 | start_row = max(1, start_row) 1065 | start_col = max(1, start_col) 1066 | 1067 | workbook = openpyxl.load_workbook(excel_path, read_only=True, data_only=True) 1068 | sheet = workbook[sheet_name] 1069 | 1070 | output_lines = [] 1071 | for row in range(start_row, end_row + 1): 1072 | row_data = [] 1073 | for col in range(start_col, end_col + 1): 1074 | cell_value = sheet.cell(row=row, column=col).value 1075 | row_data.append(str(cell_value) if cell_value is not None else "") 1076 | output_lines.append("|".join(row_data)) 1077 | 1078 | workbook.close() 1079 | del workbook 1080 | 1081 | return ("\n".join(output_lines),) 1082 | 1083 | except Exception as e: 1084 | return (f"Error: {str(e)}",) 1085 | 1086 | 1087 | #======写入表格数据 1088 | class WriteExcelData: 1089 | @classmethod 1090 | def INPUT_TYPES(cls): 1091 | return { 1092 | "required": { 1093 | "excel_path": ("STRING", {"default": "path/to/your/file.xlsx"}), 1094 | "sheet_name": ("STRING", {"default": "Sheet1"}), 1095 | "row_range": ("STRING", {"default": "2-3"}), 1096 | "col_range": ("STRING", {"default": "1-4"}), 1097 | "data": ("STRING", {"default": "", "multiline": True}), 1098 | }, 1099 | "optional": {"any": (any_typ,)} 1100 | } 1101 | 1102 | RETURN_TYPES = ("STRING",) 1103 | FUNCTION = "write_excel_data" 1104 | CATEGORY = "Meeeyo/File" 1105 | DESCRIPTION = note 1106 | def IS_CHANGED(): return float("NaN") 1107 | 1108 | def write_excel_data(self, excel_path, sheet_name, row_range, col_range, data, any=None): 1109 | try: 1110 | if not os.path.exists(excel_path): 1111 | return (f"Error: File does not exist at path: {excel_path}",) 1112 | 1113 | if not os.access(excel_path, os.W_OK): 1114 | return (f"Error: No write permission for file at path: {excel_path}",) 1115 | 1116 | if "-" in row_range: 1117 | start_row, end_row = map(int, row_range.split("-")) 1118 | else: 1119 | start_row = end_row = int(row_range) 1120 | 1121 | if "-" in col_range: 1122 | start_col, end_col = map(int, col_range.split("-")) 1123 | else: 1124 | start_col = end_col = int(col_range) 1125 | 1126 | start_row = max(1, start_row) 1127 | start_col = max(1, start_col) 1128 | 1129 | workbook = openpyxl.load_workbook(excel_path, read_only=False, data_only=True) 1130 | sheet = workbook[sheet_name] 1131 | 1132 | data_lines = data.strip().split("\n") 1133 | 1134 | for row_index, line in enumerate(data_lines, start=start_row): 1135 | if row_index > end_row: 1136 | break 1137 | 1138 | cell_values = line.split("|") 1139 | for col_index, cell_value in enumerate(cell_values, start=start_col): 1140 | if col_index > end_col: 1141 | break 1142 | 1143 | if cell_value.strip(): 1144 | sheet.cell(row=row_index, column=col_index).value = cell_value.strip() 1145 | 1146 | workbook.save(excel_path) 1147 | 1148 | workbook.close() 1149 | del workbook 1150 | 1151 | return ("Data written successfully!",) 1152 | 1153 | except PermissionError as pe: 1154 | return (f"Permission Error: {str(pe)}",) 1155 | except Exception as e: 1156 | return (f"Error: {str(e)}",) 1157 | 1158 | 1159 | #======图片插入表格 1160 | class WriteExcelImage: 1161 | @classmethod 1162 | def INPUT_TYPES(cls): 1163 | return { 1164 | "required": { 1165 | "excel_path": ("STRING", {"default": "path/to/your/file.xlsx"}), 1166 | "sheet_name": ("STRING", {"default": "Sheet1"}), 1167 | "row_range": ("STRING", {"default": "1"}), 1168 | "col_range": ("STRING", {"default": "1"}), 1169 | "image_path": ("STRING", {"default": "path/to/your/image.png"}), 1170 | }, 1171 | "optional": {"any": (any_typ,)} 1172 | } 1173 | 1174 | RETURN_TYPES = ("STRING",) 1175 | FUNCTION = "write_image" 1176 | CATEGORY = "Meeeyo/File" 1177 | DESCRIPTION = note 1178 | def IS_CHANGED(): return float("NaN") 1179 | 1180 | def write_image(self, excel_path, sheet_name, row_range, col_range, image_path, any=None): 1181 | try: 1182 | if not os.path.exists(excel_path): 1183 | return (f"Error: Excel file does not exist at path: {excel_path}",) 1184 | if not os.access(excel_path, os.W_OK): 1185 | return (f"Error: No write permission for Excel file at path: {excel_path}",) 1186 | if not os.path.exists(image_path): 1187 | return (f"Error: Image file does not exist at path: {image_path}",) 1188 | if not os.access(image_path, os.R_OK): 1189 | return (f"Error: No read permission for image file at path: {image_path}",) 1190 | if "-" in row_range: 1191 | start_row, end_row = map(int, row_range.split("-")) 1192 | else: 1193 | start_row = end_row = int(row_range) 1194 | if "-" in col_range: 1195 | start_col, end_col = map(int, col_range.split("-")) 1196 | else: 1197 | start_col = end_col = int(col_range) 1198 | start_row = max(1, start_row) 1199 | start_col = max(1, start_col) 1200 | workbook = openpyxl.load_workbook(excel_path, read_only=False, data_only=True) 1201 | sheet = workbook[sheet_name] 1202 | thumbnail_size = (128, 128) 1203 | with PILImage.open(image_path) as img: 1204 | img = img.resize(thumbnail_size) 1205 | img_byte_array = BytesIO() 1206 | img.save(img_byte_array, format='PNG') 1207 | img_byte_array.seek(0) 1208 | openpyxl_img = OpenpyxlImage(img_byte_array) 1209 | cell_address = openpyxl.utils.get_column_letter(start_col) + str(start_row) 1210 | sheet.add_image(openpyxl_img, cell_address) 1211 | workbook.save(excel_path) 1212 | workbook.close() 1213 | return ("Image inserted successfully!",) 1214 | except PermissionError as pe: 1215 | return (f"Permission Error: {str(pe)}",) 1216 | except Exception as e: 1217 | return (f"Error: {str(e)}",) 1218 | 1219 | 1220 | #======查找表格数据 1221 | class FindExcelData: 1222 | @classmethod 1223 | def INPUT_TYPES(cls): 1224 | return { 1225 | "required": { 1226 | "excel_path": ("STRING", {"default": "path/to/your/file.xlsx"}), 1227 | "sheet_name": ("STRING", {"default": "Sheet1"}), 1228 | "search_content": ("STRING", {"default": "查找内容"}), 1229 | "search_mode": (["精确查找", "模糊查找"], {"default": "精确查找"}), 1230 | }, 1231 | "optional": {"any": (any_typ,)} 1232 | } 1233 | 1234 | RETURN_TYPES = ("STRING", "INT", "INT") 1235 | FUNCTION = "find_excel_data" 1236 | CATEGORY = "Meeeyo/File" 1237 | DESCRIPTION = note 1238 | def IS_CHANGED(): return float("NaN") 1239 | 1240 | def find_excel_data(self, excel_path, sheet_name, search_content, search_mode, any=None): 1241 | try: 1242 | if not os.path.exists(excel_path): 1243 | return (f"Error: File does not exist at path: {excel_path}", None, None) 1244 | if not os.access(excel_path, os.R_OK): 1245 | return (f"Error: No read permission for file at path: {excel_path}", None, None) 1246 | workbook = openpyxl.load_workbook(excel_path, read_only=True, data_only=True) 1247 | sheet = workbook[sheet_name] 1248 | 1249 | results = [] 1250 | found_row = None 1251 | found_col = None 1252 | for row in range(1, sheet.max_row + 1): 1253 | for col in range(1, sheet.max_column + 1): 1254 | cell = sheet.cell(row=row, column=col) 1255 | cell_value = cell.value if cell.value is not None else "" 1256 | cell_value_str = str(cell_value) 1257 | if (search_mode == "精确查找" and cell_value_str == search_content) or \ 1258 | (search_mode == "模糊查找" and search_content in cell_value_str): 1259 | results.append(f"{sheet_name}|{row}|{col}|{cell_value}") 1260 | found_row = row 1261 | found_col = col 1262 | 1263 | workbook.close() 1264 | del workbook 1265 | if not results: 1266 | return ("No results found.", None, None) 1267 | return ("\n".join(results), found_row, found_col) 1268 | except Exception as e: 1269 | return (f"Error: {str(e)}", None, None) 1270 | 1271 | 1272 | #======读取表格数量差 1273 | class ReadExcelRowOrColumnDiff: 1274 | @classmethod 1275 | def INPUT_TYPES(cls): 1276 | return { 1277 | "required": { 1278 | "excel_path": ("STRING", {"default": "path/to/your/file.xlsx"}), 1279 | "sheet_name": ("STRING", {"default": "Sheet1"}), 1280 | "read_mode": (["读行", "读列"], {"default": "读行"}), 1281 | "indices": ("STRING", {"default": "1,3"}), 1282 | }, 1283 | "optional": {"any": (any_typ,)} 1284 | } 1285 | 1286 | RETURN_TYPES = ("INT",) 1287 | FUNCTION = "read_excel_row_or_column_diff" 1288 | CATEGORY = "Meeeyo/File" 1289 | DESCRIPTION = note 1290 | def IS_CHANGED(): return float("NaN") 1291 | 1292 | def read_excel_row_or_column_diff(self, excel_path, sheet_name, read_mode, indices, any=None): 1293 | try: 1294 | if not os.path.exists(excel_path): 1295 | return (f"Error: File does not exist at path: {excel_path}",) 1296 | 1297 | if not os.access(excel_path, os.R_OK): 1298 | return (f"Error: No read permission for file at path: {excel_path}",) 1299 | 1300 | workbook = openpyxl.load_workbook(excel_path, read_only=True, data_only=True) 1301 | sheet = workbook[sheet_name] 1302 | 1303 | def count_cells(mode, index): 1304 | count = 0 1305 | if mode == "读行": 1306 | for col in range(1, sheet.max_column + 1): 1307 | cell_value = sheet.cell(row=index, column=col).value 1308 | if cell_value is not None: 1309 | count += 1 1310 | else: 1311 | break 1312 | elif mode == "读列": 1313 | for row in range(1, sheet.max_row + 1): 1314 | cell_value = sheet.cell(row=row, column=index).value 1315 | if cell_value is not None: 1316 | count += 1 1317 | else: 1318 | break 1319 | return count 1320 | 1321 | indices = indices.strip() 1322 | if "," in indices: 1323 | try: 1324 | index1, index2 = map(int, indices.split(",")) 1325 | except ValueError: 1326 | return (f"Error: Invalid indices format. Please use 'number,number' format.",) 1327 | 1328 | count1 = count_cells(read_mode, index1) 1329 | count2 = count_cells(read_mode, index2) 1330 | result = count1 - count2 1331 | else: 1332 | try: 1333 | index = int(indices) 1334 | except ValueError: 1335 | return (f"Error: Invalid index format. Please enter a valid number.",) 1336 | 1337 | result = count_cells(read_mode, index) 1338 | 1339 | workbook.close() 1340 | del workbook 1341 | 1342 | return (result,) 1343 | 1344 | except Exception as e: 1345 | return (f"Error: {str(e)}",) 1346 | 1347 | 1348 | -------------------------------------------------------------------------------- /meyo_node_Functional.py: -------------------------------------------------------------------------------- 1 | import os, time, secrets, requests, base64, random 2 | import folder_paths 3 | import numpy as np 4 | from PIL import Image 5 | from datetime import datetime 6 | from . import any_typ, note 7 | 8 | 9 | 10 | #======当前时间(戳) 11 | class GetCurrentTime: 12 | @classmethod 13 | def INPUT_TYPES(cls): 14 | return { 15 | "required": { 16 | "prefix": ("STRING", {"default": ""}) 17 | }, 18 | "optional": {"any": (any_typ,)} 19 | } 20 | 21 | RETURN_TYPES = ("STRING", "INT") 22 | FUNCTION = "get_current_time" 23 | CATEGORY = "Meeeyo/Functional" 24 | DESCRIPTION = note 25 | def IS_CHANGED(): return float("NaN") 26 | 27 | def get_current_time(self, prefix, any=None): 28 | current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) 29 | timestamp = int(time.time() * 1000) 30 | formatted_time_with_prefix = f"{prefix} {current_time}" 31 | return (formatted_time_with_prefix, timestamp) 32 | 33 | 34 | #======随机整数 35 | class SimpleRandomSeed: 36 | @classmethod 37 | def INPUT_TYPES(cls): 38 | return { 39 | "optional": {"any": (any_typ,)} 40 | } 41 | 42 | RETURN_TYPES = ("STRING", "INT") 43 | FUNCTION = "generate_random_seed" 44 | CATEGORY = "Meeeyo/Functional" 45 | DESCRIPTION = note 46 | def IS_CHANGED(): return float("NaN") 47 | 48 | def generate_random_seed(self, any=None): 49 | try: 50 | length = random.randint(8, 12) 51 | first_digit = random.randint(1, 9) 52 | remaining_digits = random.randint(0, 10**(length - 1) - 1) 53 | random_seed = int(str(first_digit) + str(remaining_digits).zfill(length - 1)) 54 | return (str(random_seed), random_seed) 55 | 56 | except Exception as e: 57 | return (f"Error: {str(e)}",) 58 | 59 | 60 | #======选择参数diy 61 | class SelectionParameter: 62 | @classmethod 63 | def INPUT_TYPES(cls): 64 | return { 65 | "required": { 66 | "gender": (["男性", "女性"], {"default": "男性"}), 67 | "version": (["竖版", "横版"], {"default": "竖版"}), 68 | "additional_text": ("STRING", {"multiline": True, "default": "附加的多行文本内容"}), 69 | }, 70 | "optional": {"any": (any_typ,)} 71 | } 72 | 73 | RETURN_TYPES = ("STRING",) 74 | FUNCTION = "gender_output" 75 | CATEGORY = "Meeeyo/Functional" 76 | DESCRIPTION = note 77 | def IS_CHANGED(): return float("NaN") 78 | 79 | def gender_output(self, gender, version, additional_text, any=None): 80 | gender_value = 1 if gender == "男性" else 2 81 | version_value = 1 if version == "竖版" else 2 82 | result = f"{gender_value}+{version_value}" 83 | combined_result = f"{result}\n\n{additional_text.strip()}" 84 | return (combined_result,) 85 | 86 | 87 | #======读取页面 88 | class ReadWebNode: 89 | @classmethod 90 | def INPUT_TYPES(cls): 91 | return { 92 | "required": { 93 | "Instruction": ("STRING", {"default": ""}), 94 | "prefix_suffix": ("STRING", {"default": ""}), 95 | }, 96 | "optional": {"any": (any_typ,)} 97 | } 98 | 99 | RETURN_TYPES = ("STRING",) 100 | FUNCTION = "fetch_data" 101 | CATEGORY = "Meeeyo/Functional" 102 | DESCRIPTION = note 103 | def IS_CHANGED(): return float("NaN") 104 | 105 | def fetch_data(self, Instruction, prefix_suffix, any=None): 106 | if "|" in prefix_suffix: 107 | prefix, suffix = prefix_suffix.split("|", 1) 108 | else: 109 | prefix = prefix_suffix 110 | suffix = "" 111 | modified_url = f"{base64.b64decode('aHR0cHM6Ly93d3cubWVlZXlvLmNvbS91L2dldG5vZGUv').decode()}{Instruction.lower()}{base64.b64decode('LnBocA==').decode()}" 112 | 113 | try: 114 | token = secrets.token_hex(16) 115 | headers = {'Authorization': f'Bearer {token}'} 116 | response = requests.get(modified_url, headers=headers) 117 | response.raise_for_status() 118 | response_text = f"{prefix}{response.text}{suffix}" 119 | return (response_text,) 120 | except requests.RequestException as e: 121 | return ('Error!解析失败,请检查后重试!',) 122 | 123 | 124 | #===VAE解码预览 125 | class DecodePreview: 126 | @classmethod 127 | def INPUT_TYPES(cls): 128 | return { 129 | "required": { 130 | "latent": ("LATENT",), 131 | "vae": ("VAE",) 132 | }, 133 | } 134 | 135 | RETURN_TYPES = ("IMAGE",) 136 | FUNCTION = "preview" 137 | OUTPUT_NODE = True 138 | CATEGORY = "Meeeyo/Functional" 139 | DESCRIPTION = note 140 | def IS_CHANGED(): return float("NaN") 141 | 142 | def preview(self, latent, vae, filename_prefix="Preview", prompt=None, extra_pnginfo=None): 143 | images = vae.decode(latent["samples"]) 144 | save_path, filename, counter, _, _ = folder_paths.get_save_image_path( 145 | filename_prefix, folder_paths.get_temp_directory(), images[0].shape[1], images[0].shape[0] 146 | ) 147 | results = [] 148 | for img in images: 149 | img_pil = Image.fromarray(np.clip(255.0 * img.cpu().numpy(), 0, 255).astype(np.uint8)) 150 | file_path = os.path.join(save_path, f"{filename}_{counter:05}.png") 151 | img_pil.save(file_path, compress_level=0) 152 | 153 | results.append({ 154 | "filename": f"{filename}_{counter:05}.png", 155 | "subfolder": os.path.relpath(save_path, folder_paths.get_temp_directory()), 156 | "type": "temp" 157 | }) 158 | counter += 1 159 | 160 | return {"ui": {"images": results}, "result": (images,)} 161 | 162 | -------------------------------------------------------------------------------- /meyo_node_String.py: -------------------------------------------------------------------------------- 1 | import re, math, datetime, random, secrets, requests, string 2 | from . import any_typ, note 3 | 4 | 5 | 6 | #======文本输入 7 | class SingleTextInput: 8 | @classmethod 9 | def INPUT_TYPES(cls): 10 | return { 11 | "required": { 12 | "text": ("STRING", {"default": "", "multiline": True}), 13 | } 14 | } 15 | 16 | RETURN_TYPES = ("STRING",) 17 | FUNCTION = "process_input" 18 | OUTPUT_NODE = False 19 | CATEGORY = "Meeeyo/String" 20 | DESCRIPTION = note 21 | def IS_CHANGED(): return float("NaN") 22 | 23 | def process_input(self, text): 24 | return (text,) 25 | 26 | 27 | #======文本到列表 28 | class TextToList: 29 | @classmethod 30 | def INPUT_TYPES(cls): 31 | return { 32 | "required": { 33 | "text": ("STRING", {"multiline": True, "default": ""}), 34 | "delimiter": ("STRING", {"default": ""}), 35 | } 36 | } 37 | 38 | RETURN_TYPES = ("STRING",) 39 | FUNCTION = "split_text" 40 | OUTPUT_IS_LIST = (True,) 41 | CATEGORY = "Meeeyo/String" 42 | DESCRIPTION = note 43 | def IS_CHANGED(): return float("NaN") 44 | 45 | def split_text(self, text, delimiter): 46 | if not delimiter: 47 | parts = text.split('\n') 48 | else: 49 | parts = text.split(delimiter) 50 | parts = [part.strip() for part in parts if part.strip()] 51 | if not parts: 52 | return ([],) 53 | return (parts,) 54 | 55 | 56 | #======文本拼接 57 | class TextConcatenator: 58 | @classmethod 59 | def INPUT_TYPES(cls): 60 | return { 61 | "optional": { 62 | "text1": ("STRING", {"multiline": False, "default": ""}), 63 | "text2": ("STRING", {"multiline": False, "default": ""}), 64 | "text3": ("STRING", {"multiline": False, "default": ""}), 65 | "text4": ("STRING", {"multiline": False, "default": ""}), 66 | "combine_order": ("STRING", {"default": ""}), 67 | "separator": ("STRING", {"default": ","}) 68 | }, 69 | } 70 | 71 | RETURN_TYPES = ("STRING",) 72 | FUNCTION = "combine_texts" 73 | CATEGORY = "Meeeyo/String" 74 | DESCRIPTION = note 75 | def IS_CHANGED(): return float("NaN") 76 | 77 | def combine_texts(self, text1, text2, text3, text4, combine_order, separator): 78 | try: 79 | text_map = { 80 | "1": text1, 81 | "2": text2, 82 | "3": text3, 83 | "4": text4 84 | } 85 | if not combine_order: 86 | combine_order = "1+2+3+4" 87 | parts = combine_order.split("+") 88 | valid_parts = [] 89 | for part in parts: 90 | if part in text_map: 91 | valid_parts.append(part) 92 | else: 93 | return (f"Error: Invalid input '{part}' in combine_order. Valid options are 1, 2, 3, 4.",) 94 | non_empty_texts = [text_map[part] for part in valid_parts if text_map[part]] 95 | 96 | if separator == '\\n': 97 | separator = '\n' 98 | 99 | result = separator.join(non_empty_texts) 100 | return (result,) 101 | except Exception as e: 102 | return (f"Error: {str(e)}",) 103 | 104 | 105 | #======多参数输入 106 | class MultiParamInputNode: 107 | @classmethod 108 | def INPUT_TYPES(cls): 109 | return { 110 | "required": { 111 | "text1": ("STRING", {"default": "", "multiline": True}), 112 | "text2": ("STRING", {"default": "", "multiline": True}), 113 | "int1": ("INT", {"default": 0, "min": -1000000, "max": 1000000}), 114 | "int2": ("INT", {"default": 0, "min": -1000000, "max": 1000000}), 115 | } 116 | } 117 | 118 | RETURN_TYPES = ("STRING", "STRING", "INT", "INT") 119 | FUNCTION = "process_inputs" 120 | OUTPUT_NODE = False 121 | CATEGORY = "Meeeyo/String" 122 | DESCRIPTION = note 123 | def IS_CHANGED(): return float("NaN") 124 | 125 | def process_inputs(self, text1, text2, int1, int2): 126 | return (text1, text2, int1, int2) 127 | 128 | 129 | #======整数参数 130 | class NumberExtractor: 131 | @classmethod 132 | def INPUT_TYPES(cls): 133 | return { 134 | "required": { 135 | "input_text": ("STRING", {"default": "2|3"}), 136 | }, 137 | "optional": {}, 138 | } 139 | 140 | RETURN_TYPES = ("INT", "INT") 141 | FUNCTION = "extract_lines_by_index" 142 | OUTPUT_TYPES = ("INT", "INT") 143 | CATEGORY = "Meeeyo/String" 144 | DESCRIPTION = note 145 | def IS_CHANGED(): return float("NaN") 146 | 147 | def extract_lines_by_index(self, input_text): 148 | try: 149 | data_list = input_text.split("|") 150 | 151 | result = [] 152 | for i in range(2): 153 | if i < len(data_list): 154 | try: 155 | result.append(int(data_list[i])) 156 | except ValueError: 157 | result.append(0) 158 | else: 159 | result.append(0) 160 | 161 | return tuple(result) 162 | except: 163 | return (0, 0) 164 | 165 | 166 | #======添加前后缀 167 | class AddPrefixSuffix: 168 | @classmethod 169 | def INPUT_TYPES(cls): 170 | return { 171 | "required": { 172 | "input_string": ("STRING", {"default": ""}), 173 | "prefix": ("STRING", {"default": "前缀符"}), 174 | "suffix": ("STRING", {"default": "后缀符"}), 175 | }, 176 | "optional": {}, 177 | } 178 | 179 | RETURN_TYPES = ("STRING",) 180 | FUNCTION = "add_prefix_suffix" 181 | CATEGORY = "Meeeyo/String" 182 | DESCRIPTION = note 183 | def IS_CHANGED(): return float("NaN") 184 | 185 | def add_prefix_suffix(self, input_string, prefix, suffix): 186 | return (f"{prefix}{input_string}{suffix}",) 187 | 188 | 189 | #======提取标签之间 190 | class ExtractSubstring: 191 | @classmethod 192 | def INPUT_TYPES(cls): 193 | return { 194 | "required": { 195 | "input_string": ("STRING", {"multiline": True, "default": ""}), 196 | "pattern": ("STRING", {"default": "标签始|标签尾"}), 197 | }, 198 | "optional": {}, 199 | } 200 | 201 | RETURN_TYPES = ("STRING",) 202 | FUNCTION = "extract_substring" 203 | CATEGORY = "Meeeyo/String" 204 | DESCRIPTION = note 205 | def IS_CHANGED(): return float("NaN") 206 | 207 | def extract_substring(self, input_string, pattern): 208 | try: 209 | parts = pattern.split('|') 210 | start_str = parts[0] 211 | end_str = parts[1] if len(parts) > 1 and parts[1].strip() else "\n" 212 | 213 | start_index = input_string.index(start_str) + len(start_str) 214 | 215 | end_index = input_string.find(end_str, start_index) 216 | if end_index == -1: 217 | end_index = input_string.find("\n", start_index) 218 | if end_index == -1: 219 | end_index = len(input_string) 220 | 221 | extracted = input_string[start_index:end_index] 222 | 223 | lines = extracted.splitlines() 224 | non_empty_lines = [line for line in lines if line.strip()] 225 | result = '\n'.join(non_empty_lines) 226 | 227 | return (result,) 228 | except ValueError: 229 | return ("",) 230 | 231 | 232 | #======按数字范围提取 233 | class ExtractSubstringByIndices: 234 | @classmethod 235 | def INPUT_TYPES(cls): 236 | return { 237 | "required": { 238 | "input_string": ("STRING", {"default": ""}), 239 | "indices": ("STRING", {"default": "2-6"}), 240 | "direction": (["从前面", "从后面"], {"default": "从前面"}), 241 | }, 242 | "optional": {}, 243 | } 244 | 245 | RETURN_TYPES = ("STRING",) 246 | FUNCTION = "extract_substring_by_indices" 247 | CATEGORY = "Meeeyo/String" 248 | DESCRIPTION = note 249 | def IS_CHANGED(): return float("NaN") 250 | 251 | def extract_substring_by_indices(self, input_string, indices, direction): 252 | try: 253 | if '-' in indices: 254 | start_index, end_index = map(int, indices.split('-')) 255 | else: 256 | start_index = end_index = int(indices) 257 | 258 | start_index -= 1 259 | end_index -= 1 260 | 261 | if start_index < 0 or start_index >= len(input_string): 262 | return ("",) 263 | 264 | if end_index < 0 or end_index >= len(input_string): 265 | end_index = len(input_string) - 1 266 | 267 | if start_index > end_index: 268 | start_index, end_index = end_index, start_index 269 | 270 | if direction == "从前面": 271 | return (input_string[start_index:end_index + 1],) 272 | elif direction == "从后面": 273 | start_index = len(input_string) - start_index - 1 274 | end_index = len(input_string) - end_index - 1 275 | if start_index > end_index: 276 | start_index, end_index = end_index, start_index 277 | return (input_string[start_index:end_index + 1],) 278 | except ValueError: 279 | return ("",) 280 | 281 | 282 | #======分隔符拆分两边 283 | class SplitStringByDelimiter: 284 | @classmethod 285 | def INPUT_TYPES(cls): 286 | return { 287 | "required": { 288 | "input_string": ("STRING", {"default": "文本|内容"}), 289 | "delimiter": ("STRING", {"default": "|"}), 290 | }, 291 | "optional": {}, 292 | } 293 | 294 | RETURN_TYPES = ("STRING", "STRING") 295 | FUNCTION = "split_string_by_delimiter" 296 | CATEGORY = "Meeeyo/String" 297 | DESCRIPTION = note 298 | def IS_CHANGED(): return float("NaN") 299 | 300 | def split_string_by_delimiter(self, input_string, delimiter): 301 | parts = input_string.split(delimiter, 1) 302 | if len(parts) == 2: 303 | return (parts[0], parts[1]) 304 | else: 305 | return (input_string, "") 306 | 307 | 308 | #======常规处理字符 309 | class ProcessString: 310 | @classmethod 311 | def INPUT_TYPES(cls): 312 | return { 313 | "required": { 314 | "input_string": ("STRING", {"multiline": True, "default": ""}), 315 | "option": (["不改变", "取数字", "取字母", "转大写", "转小写", "取中文", "去标点", "去换行", "去空行", "去空格", "去格式", "统计字数"], {"default": "不改变"}), 316 | }, 317 | "optional": {}, 318 | } 319 | 320 | RETURN_TYPES = ("STRING",) 321 | FUNCTION = "process_string" 322 | CATEGORY = "Meeeyo/String" 323 | DESCRIPTION = note 324 | def IS_CHANGED(): return float("NaN") 325 | 326 | def process_string(self, input_string, option): 327 | if option == "取数字": 328 | result = ''.join(re.findall(r'\d', input_string)) 329 | elif option == "取字母": 330 | result = ''.join(filter(lambda char: char.isalpha() and not self.is_chinese(char), input_string)) 331 | elif option == "转大写": 332 | result = input_string.upper() 333 | elif option == "转小写": 334 | result = input_string.lower() 335 | elif option == "取中文": 336 | result = ''.join(filter(self.is_chinese, input_string)) 337 | elif option == "去标点": 338 | result = re.sub(r'[^\w\s\u4e00-\u9fff]', '', input_string) 339 | elif option == "去换行": 340 | result = input_string.replace('\n', '') 341 | elif option == "去空行": 342 | result = '\n'.join(filter(lambda line: line.strip(), input_string.splitlines())) 343 | elif option == "去空格": 344 | result = input_string.replace(' ', '') 345 | elif option == "去格式": 346 | result = re.sub(r'\s+', '', input_string) 347 | elif option == "统计字数": 348 | result = str(len(input_string)) 349 | else: 350 | result = input_string 351 | 352 | return (result,) 353 | 354 | @staticmethod 355 | def is_chinese(char): 356 | return '\u4e00' <= char <= '\u9fff' 357 | 358 | 359 | #======提取前后字符 360 | class ExtractBeforeAfter: 361 | @classmethod 362 | def INPUT_TYPES(cls): 363 | return { 364 | "required": { 365 | "input_string": ("STRING", {"default": ""}), 366 | "pattern": ("STRING", {"default": "标签符"}), 367 | "position": (["保留最初之前", "保留最初之后", "保留最后之前", "保留最后之后"], {"default": "保留最初之前"}), 368 | "include_delimiter": ("BOOLEAN", {"default": False}), 369 | }, 370 | "optional": {}, 371 | } 372 | 373 | RETURN_TYPES = ("STRING",) 374 | FUNCTION = "extract_before_after" 375 | CATEGORY = "Meeeyo/String" 376 | DESCRIPTION = note 377 | def IS_CHANGED(): return float("NaN") 378 | 379 | def extract_before_after(self, input_string, pattern, position, include_delimiter): 380 | if position == "保留最初之前": 381 | index = input_string.find(pattern) 382 | if index != -1: 383 | result = input_string[:index + len(pattern) if include_delimiter else index] 384 | return (result,) 385 | elif position == "保留最初之后": 386 | index = input_string.find(pattern) 387 | if index != -1: 388 | result = input_string[index:] if include_delimiter else input_string[index + len(pattern):] 389 | return (result,) 390 | elif position == "保留最后之前": 391 | index = input_string.rfind(pattern) 392 | if index != -1: 393 | result = input_string[:index + len(pattern) if include_delimiter else index] 394 | return (result,) 395 | elif position == "保留最后之后": 396 | index = input_string.rfind(pattern) 397 | if index != -1: 398 | result = input_string[index:] if include_delimiter else input_string[index + len(pattern):] 399 | return (result,) 400 | return ("",) 401 | 402 | 403 | #======简易文本替换 404 | class SimpleTextReplacer: 405 | @classmethod 406 | def INPUT_TYPES(cls): 407 | return { 408 | "required": { 409 | "input_string": ("STRING", {"multiline": True, "default": "", "forceInput": True}), 410 | "find_text": ("STRING", {"default": ""}), 411 | "replace_text": ("STRING", {"default": ""}) 412 | }, 413 | } 414 | 415 | RETURN_TYPES = ("STRING",) 416 | FUNCTION = "replace_text" 417 | CATEGORY = "Meeeyo/String" 418 | DESCRIPTION = note 419 | def IS_CHANGED(): return float("NaN") 420 | 421 | def replace_text(self, input_string, find_text, replace_text): 422 | try: 423 | if not find_text: 424 | return (input_string,) 425 | 426 | if replace_text == '\\n': 427 | replace_text = '\n' 428 | 429 | result = input_string.replace(find_text, replace_text) 430 | return (result,) 431 | except Exception as e: 432 | return (f"Error: {str(e)}",) 433 | 434 | 435 | #======替换第n次出现 436 | class ReplaceNthOccurrence: 437 | @classmethod 438 | def INPUT_TYPES(cls): 439 | return { 440 | "required": { 441 | "original_text": ("STRING", {"multiline": True, "default": ""}), 442 | "occurrence": ("INT", {"default": 1, "min": 0}), 443 | "search_str": ("STRING", {"default": "替换前字符"}), 444 | "replace_str": ("STRING", {"default": "替换后字符"}), 445 | }, 446 | "optional": {}, 447 | } 448 | 449 | RETURN_TYPES = ("STRING",) 450 | FUNCTION = "replace_nth_occurrence" 451 | CATEGORY = "Meeeyo/String" 452 | DESCRIPTION = note 453 | def IS_CHANGED(): return float("NaN") 454 | 455 | def replace_nth_occurrence(self, original_text, occurrence, search_str, replace_str): 456 | if occurrence == 0: 457 | result = original_text.replace(search_str, replace_str) 458 | else: 459 | def replace_nth_match(match): 460 | nonlocal occurrence 461 | occurrence -= 1 462 | return replace_str if occurrence == 0 else match.group(0) 463 | 464 | result = re.sub(re.escape(search_str), replace_nth_match, original_text, count=occurrence) 465 | 466 | return (result,) 467 | 468 | 469 | #======多次出现依次替换 470 | class ReplaceMultiple: 471 | @classmethod 472 | def INPUT_TYPES(cls): 473 | return { 474 | "required": { 475 | "original_text": ("STRING", {"multiline": True, "default": ""}), 476 | "replacement_rule": ("STRING", {"default": ""}), 477 | }, 478 | "optional": {}, 479 | } 480 | 481 | RETURN_TYPES = ("STRING",) 482 | FUNCTION = "replace_multiple" 483 | CATEGORY = "Meeeyo/String" 484 | DESCRIPTION = note 485 | def IS_CHANGED(): return float("NaN") 486 | 487 | def replace_multiple(self, original_text, replacement_rule): 488 | try: 489 | search_str, replacements = replacement_rule.split('|') 490 | replacements = [rep for rep in replacements.split(',') if rep] 491 | 492 | def replace_match(match): 493 | nonlocal replacements 494 | if replacements: 495 | return replacements.pop(0) 496 | return match.group(0) 497 | 498 | result = re.sub(re.escape(search_str), replace_match, original_text) 499 | 500 | return (result,) 501 | except ValueError: 502 | return ("",) 503 | 504 | 505 | #======批量替换字符 506 | class BatchReplaceStrings: 507 | @classmethod 508 | def INPUT_TYPES(cls): 509 | return { 510 | "required": { 511 | "original_text": ("STRING", {"multiline": False, "default": "文本内容"}), 512 | "replacement_rules": ("STRING", {"multiline": True, "default": ""}), 513 | }, 514 | "optional": {}, 515 | } 516 | 517 | RETURN_TYPES = ("STRING",) 518 | FUNCTION = "batch_replace_strings" 519 | CATEGORY = "Meeeyo/String" 520 | DESCRIPTION = note 521 | def IS_CHANGED(): return float("NaN") 522 | 523 | def batch_replace_strings(self, original_text, replacement_rules): 524 | rules = replacement_rules.strip().split('\n') 525 | for rule in rules: 526 | if '|' in rule: 527 | search_strs, replace_str = rule.split('|', 1) 528 | 529 | search_strs = search_strs.replace("\\n", "\n") 530 | replace_str = replace_str.replace("\\n", "\n") 531 | 532 | search_strs = search_strs.split(',') 533 | 534 | for search_str in search_strs: 535 | original_text = original_text.replace(search_str, replace_str) 536 | return (original_text,) 537 | 538 | 539 | #======随机行内容 540 | class RandomLineFromText: 541 | @classmethod 542 | def INPUT_TYPES(cls): 543 | return { 544 | "required": { 545 | "input_text": ("STRING", {"multiline": True, "default": ""}), 546 | }, 547 | "optional": {"any": (any_typ,)} 548 | } 549 | 550 | RETURN_TYPES = ("STRING",) 551 | FUNCTION = "get_random_line" 552 | CATEGORY = "Meeeyo/String" 553 | DESCRIPTION = note 554 | def IS_CHANGED(): return float("NaN") 555 | 556 | def get_random_line(self, input_text, any=None): 557 | lines = input_text.strip().splitlines() 558 | if not lines: 559 | return ("",) 560 | return (random.choice(lines),) 561 | 562 | 563 | #======判断是否包含字符 564 | class CheckSubstringPresence: 565 | @classmethod 566 | def INPUT_TYPES(cls): 567 | return { 568 | "required": { 569 | "input_text": ("STRING", {"default": "文本内容"}), 570 | "substring": ("STRING", {"default": "查找符1|查找符2"}), 571 | "mode": (["同时满足", "任意满足"], {"default": "任意满足"}), 572 | }, 573 | "optional": {}, 574 | } 575 | 576 | RETURN_TYPES = ("INT",) 577 | FUNCTION = "check_substring_presence" 578 | CATEGORY = "Meeeyo/String" 579 | DESCRIPTION = note 580 | def IS_CHANGED(): return float("NaN") 581 | 582 | def check_substring_presence(self, input_text, substring, mode): 583 | substrings = substring.split('|') 584 | 585 | if mode == "同时满足": 586 | for sub in substrings: 587 | if sub not in input_text: 588 | return (0,) 589 | return (1,) 590 | elif mode == "任意满足": 591 | for sub in substrings: 592 | if sub in input_text: 593 | return (1,) 594 | return (0,) 595 | 596 | 597 | #======段落每行添加前后缀 598 | class AddPrefixSuffixToLines: 599 | @classmethod 600 | def INPUT_TYPES(cls): 601 | return { 602 | "required": { 603 | "input_text": ("STRING", {"multiline": True, "default": ""}), 604 | "prefix_suffix": ("STRING", {"default": "前缀符|后缀符"}), 605 | }, 606 | "optional": {}, 607 | } 608 | 609 | RETURN_TYPES = ("STRING",) 610 | FUNCTION = "add_prefix_suffix_to_lines" 611 | CATEGORY = "Meeeyo/String" 612 | DESCRIPTION = note 613 | def IS_CHANGED(): return float("NaN") 614 | 615 | def add_prefix_suffix_to_lines(self, prefix_suffix, input_text): 616 | try: 617 | prefix, suffix = prefix_suffix.split('|') 618 | lines = input_text.splitlines() 619 | modified_lines = [f"{prefix}{line}{suffix}" for line in lines] 620 | result = '\n'.join(modified_lines) 621 | return (result,) 622 | except ValueError: 623 | return ("",) 624 | 625 | 626 | #======段落提取指定索引行 627 | class ExtractAndCombineLines: 628 | @classmethod 629 | def INPUT_TYPES(cls): 630 | return { 631 | "required": { 632 | "input_text": ("STRING", {"multiline": True, "default": ""}), 633 | "line_indices": ("STRING", {"default": "2-3"}), 634 | }, 635 | "optional": {}, 636 | } 637 | 638 | RETURN_TYPES = ("STRING",) 639 | FUNCTION = "extract_and_combine_lines" 640 | CATEGORY = "Meeeyo/String" 641 | DESCRIPTION = note 642 | def IS_CHANGED(): return float("NaN") 643 | 644 | def extract_and_combine_lines(self, input_text, line_indices): 645 | try: 646 | lines = input_text.splitlines() 647 | result_lines = [] 648 | 649 | if '-' in line_indices: 650 | start, end = map(int, line_indices.split('-')) 651 | start = max(1, start) 652 | end = min(len(lines), end) 653 | result_lines = lines[start - 1:end] 654 | else: 655 | indices = map(int, line_indices.split('|')) 656 | for index in indices: 657 | if 1 <= index <= len(lines): 658 | result_lines.append(lines[index - 1]) 659 | 660 | result = '\n'.join(result_lines) 661 | return (result,) 662 | except ValueError: 663 | return ("",) 664 | 665 | 666 | #======段落提取或移除字符行 667 | class FilterLinesBySubstrings: 668 | @classmethod 669 | def INPUT_TYPES(cls): 670 | return { 671 | "required": { 672 | "input_text": ("STRING", {"multiline": True, "default": ""}), 673 | "substrings": ("STRING", {"default": "查找符1|查找符2"}), 674 | "action": (["保留", "移除"], {"default": "保留"}), 675 | }, 676 | "optional": {}, 677 | } 678 | 679 | RETURN_TYPES = ("STRING",) 680 | FUNCTION = "filter_lines_by_substrings" 681 | CATEGORY = "Meeeyo/String" 682 | DESCRIPTION = note 683 | def IS_CHANGED(): return float("NaN") 684 | 685 | def filter_lines_by_substrings(self, input_text, substrings, action): 686 | lines = input_text.splitlines() 687 | substring_list = substrings.split('|') 688 | result_lines = [] 689 | 690 | for line in lines: 691 | contains_substring = any(substring in line for substring in substring_list) 692 | if (action == "保留" and contains_substring) or (action == "移除" and not contains_substring): 693 | result_lines.append(line) 694 | 695 | non_empty_lines = [line for line in result_lines if line.strip()] 696 | result = '\n'.join(non_empty_lines) 697 | return (result,) 698 | 699 | 700 | #======根据字数范围过滤文本行 701 | class FilterLinesByWordCount: 702 | @classmethod 703 | def INPUT_TYPES(cls): 704 | return { 705 | "required": { 706 | "input_text": ("STRING", {"multiline": True, "default": ""}), 707 | "word_count_range": ("STRING", {"default": "2-10"}), 708 | }, 709 | "optional": {}, 710 | } 711 | 712 | RETURN_TYPES = ("STRING",) 713 | FUNCTION = "filter_lines_by_word_count" 714 | CATEGORY = "Meeeyo/String" 715 | DESCRIPTION = note 716 | def IS_CHANGED(): return float("NaN") 717 | 718 | def filter_lines_by_word_count(self, input_text, word_count_range): 719 | try: 720 | lines = input_text.splitlines() 721 | result_lines = [] 722 | 723 | if '-' in word_count_range: 724 | min_count, max_count = map(int, word_count_range.split('-')) 725 | result_lines = [line for line in lines if min_count <= len(line) <= max_count] 726 | 727 | result = '\n'.join(result_lines) 728 | return (result,) 729 | except ValueError: 730 | return ("",) 731 | 732 | 733 | #======按序号提取分割文本 734 | class SplitAndExtractText: 735 | @classmethod 736 | def INPUT_TYPES(cls): 737 | return { 738 | "required": { 739 | "input_text": ("STRING", {"multiline": True, "default": ""}), 740 | "delimiter": ("STRING", {"default": "分隔符"}), 741 | "index": ("INT", {"default": 1, "min": 1}), 742 | "order": (["顺序", "倒序"], {"default": "顺序"}), 743 | "include_delimiter": ("BOOLEAN", {"default": False}), 744 | }, 745 | "optional": {}, 746 | } 747 | 748 | RETURN_TYPES = ("STRING",) 749 | FUNCTION = "split_and_extract" 750 | CATEGORY = "Meeeyo/String" 751 | DESCRIPTION = note 752 | def IS_CHANGED(): return float("NaN") 753 | 754 | def split_and_extract(self, input_text, delimiter, index, order, include_delimiter): 755 | try: 756 | if not delimiter: 757 | parts = input_text.splitlines() 758 | else: 759 | parts = input_text.split(delimiter) 760 | 761 | if order == "倒序": 762 | parts = parts[::-1] 763 | 764 | if index > 0 and index <= len(parts): 765 | selected_part = parts[index - 1] 766 | 767 | if include_delimiter and delimiter: 768 | if order == "顺序": 769 | if index > 1: 770 | selected_part = delimiter + selected_part 771 | if index < len(parts): 772 | selected_part += delimiter 773 | elif order == "倒序": 774 | if index > 1: 775 | selected_part += delimiter 776 | if index < len(parts): 777 | selected_part = delimiter + selected_part 778 | 779 | lines = selected_part.splitlines() 780 | non_empty_lines = [line for line in lines if line.strip()] 781 | result = '\n'.join(non_empty_lines) 782 | return (result,) 783 | else: 784 | return ("",) 785 | except ValueError: 786 | return ("",) 787 | 788 | 789 | #======文本出现次数 790 | class CountOccurrences: 791 | @classmethod 792 | def INPUT_TYPES(cls): 793 | return { 794 | "required": { 795 | "input_text": ("STRING", {"multiline": True, "default": ""}), 796 | "char": ("STRING", {"default": "查找符"}), 797 | }, 798 | "optional": {}, 799 | } 800 | 801 | RETURN_TYPES = ("INT", "STRING") 802 | FUNCTION = "count_text_segments" 803 | CATEGORY = "Meeeyo/String" 804 | DESCRIPTION = note 805 | def IS_CHANGED(): return float("NaN") 806 | 807 | def count_text_segments(self, input_text, char): 808 | try: 809 | if char == "\\n": 810 | lines = [line for line in input_text.splitlines() if line.strip()] 811 | count = len(lines) 812 | else: 813 | count = input_text.count(char) 814 | return (count, str(count)) 815 | except ValueError: 816 | return (0, "0") 817 | 818 | 819 | #======文本拆分 820 | class ExtractLinesByIndex: 821 | @classmethod 822 | def INPUT_TYPES(cls): 823 | return { 824 | "required": { 825 | "input_text": ("STRING", {"multiline": True, "default": ""}), 826 | "delimiter": ("STRING", {"default": "标签符"}), 827 | "index": ("INT", {"default": 1, "min": 1}), 828 | }, 829 | "optional": {}, 830 | } 831 | 832 | RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING") 833 | FUNCTION = "extract_lines_by_index" 834 | OUTPUT_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING") 835 | OUTPUT_NAMES = ("文本1", "文本2", "文本3", "文本4", "文本5") 836 | CATEGORY = "Meeeyo/String" 837 | DESCRIPTION = note 838 | def IS_CHANGED(): return float("NaN") 839 | 840 | def extract_lines_by_index(self, input_text, index, delimiter): 841 | try: 842 | if delimiter == "" or delimiter == "\n": 843 | lines = input_text.splitlines() 844 | else: 845 | lines = input_text.split(delimiter) 846 | 847 | result_lines = [] 848 | 849 | for i in range(index - 1, index + 4): 850 | if 0 <= i < len(lines): 851 | result_lines.append(lines[i]) 852 | else: 853 | result_lines.append("") 854 | 855 | return tuple(result_lines) 856 | except ValueError: 857 | return ("", "", "", "", "") 858 | 859 | 860 | #======提取特定行 861 | class ExtractSpecificLines: 862 | @classmethod 863 | def INPUT_TYPES(cls): 864 | return { 865 | "required": { 866 | "input_text": ("STRING", {"multiline": True, "default": ""}), 867 | "line_indices": ("STRING", {"default": "1|2"}), 868 | "split_char": ("STRING", {"default": "\n"}), 869 | }, 870 | "optional": {}, 871 | } 872 | 873 | RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING") 874 | FUNCTION = "extract_specific_lines" 875 | CATEGORY = "Meeeyo/String" 876 | DESCRIPTION = note 877 | def IS_CHANGED(): return float("NaN") 878 | 879 | def extract_specific_lines(self, input_text, line_indices, split_char): 880 | if not split_char or split_char == "\n": 881 | lines = input_text.split('\n') 882 | else: 883 | lines = input_text.split(split_char) 884 | 885 | indices = [int(index) - 1 for index in line_indices.split('|') if index.isdigit()] 886 | 887 | results = [] 888 | for index in indices: 889 | if 0 <= index < len(lines): 890 | results.append(lines[index]) 891 | else: 892 | results.append("") 893 | 894 | while len(results) < 5: 895 | results.append("") 896 | 897 | non_empty_results = [result for result in results[:5] if result.strip()] 898 | if not split_char or split_char == "\n": 899 | combined_result = '\n'.join(non_empty_results) 900 | else: 901 | combined_result = split_char.join(non_empty_results) 902 | 903 | return tuple(results[:5] + [combined_result]) 904 | 905 | 906 | #======删除标签内的内容 907 | class RemoveContentBetweenChars: 908 | @classmethod 909 | def INPUT_TYPES(cls): 910 | return { 911 | "required": { 912 | "input_text": ("STRING", {"multiline": True, "default": ""}), 913 | "chars": ("STRING", {"default": "(|)"}), 914 | }, 915 | "optional": {}, 916 | } 917 | 918 | RETURN_TYPES = ("STRING",) 919 | FUNCTION = "remove_content_between_chars" 920 | CATEGORY = "Meeeyo/String" 921 | DESCRIPTION = note 922 | def IS_CHANGED(): return float("NaN") 923 | 924 | def remove_content_between_chars(self, input_text, chars): 925 | try: 926 | if len(chars) == 3 and chars[1] == '|': 927 | start_char, end_char = chars[0], chars[2] 928 | else: 929 | return input_text 930 | 931 | pattern = re.escape(start_char) + '.*?' + re.escape(end_char) 932 | result = re.sub(pattern, '', input_text) 933 | 934 | return (result,) 935 | except ValueError: 936 | return ("",) 937 | 938 | 939 | #======随机打乱 940 | class ShuffleTextLines: 941 | @classmethod 942 | def INPUT_TYPES(cls): 943 | return { 944 | "required": { 945 | "input_text": ("STRING", {"multiline": True, "default": ""}), 946 | "delimiter": ("STRING", {"default": "分隔符"}), 947 | }, 948 | "optional": {"any": (any_typ,)} 949 | } 950 | 951 | RETURN_TYPES = ("STRING",) 952 | FUNCTION = "shuffle_text_lines" 953 | CATEGORY = "Meeeyo/String" 954 | DESCRIPTION = note 955 | def IS_CHANGED(): return float("NaN") 956 | 957 | def shuffle_text_lines(self, input_text, delimiter, any=None): 958 | if delimiter == "": 959 | lines = input_text.splitlines() 960 | elif delimiter == "\n": 961 | lines = input_text.split("\n") 962 | else: 963 | lines = input_text.split(delimiter) 964 | 965 | lines = [line for line in lines if line.strip()] 966 | 967 | random.shuffle(lines) 968 | 969 | if delimiter == "": 970 | result = "\n".join(lines) 971 | elif delimiter == "\n": 972 | result = "\n".join(lines) 973 | else: 974 | result = delimiter.join(lines) 975 | 976 | return (result,) 977 | 978 | 979 | #======判断返回内容 980 | class ConditionalTextOutput: 981 | @classmethod 982 | def INPUT_TYPES(cls): 983 | return { 984 | "required": { 985 | "original_content": ("STRING", {"multiline": True, "default": ""}), 986 | "check_text": ("STRING", {"default": "查找字符"}), 987 | "text_if_exists": ("STRING", {"default": "存在返回内容"}), 988 | "text_if_not_exists": ("STRING", {"default": "不存在返回内容"}), 989 | }, 990 | "optional": {}, 991 | } 992 | 993 | RETURN_TYPES = ("STRING",) 994 | FUNCTION = "conditional_text_output" 995 | CATEGORY = "Meeeyo/String" 996 | DESCRIPTION = note 997 | def IS_CHANGED(): return float("NaN") 998 | 999 | def conditional_text_output(self, original_content, check_text, text_if_exists, text_if_not_exists): 1000 | if not check_text: 1001 | return ("",) 1002 | 1003 | if check_text in original_content: 1004 | return (text_if_exists,) 1005 | else: 1006 | return (text_if_not_exists,) 1007 | 1008 | 1009 | #======文本按条件判断 1010 | class TextConditionCheck: 1011 | @classmethod 1012 | def INPUT_TYPES(cls): 1013 | return { 1014 | "required": { 1015 | "original_content": ("STRING", {"multiline": True, "default": ""}), # 输入多行文本 1016 | "length_condition": ("STRING", {"default": "3-6"}), 1017 | "frequency_condition": ("STRING", {"default": ""}), 1018 | }, 1019 | "optional": {}, 1020 | } 1021 | 1022 | RETURN_TYPES = ("INT", "STRING") 1023 | FUNCTION = "text_condition_check" 1024 | CATEGORY = "Meeeyo/String" 1025 | DESCRIPTION = note 1026 | def IS_CHANGED(): return float("NaN") 1027 | 1028 | def text_condition_check(self, original_content, length_condition, frequency_condition): 1029 | length_valid = self.check_length_condition(original_content, length_condition) 1030 | 1031 | frequency_valid = self.check_frequency_condition(original_content, frequency_condition) 1032 | 1033 | if length_valid and frequency_valid: 1034 | return (1, "1") 1035 | else: 1036 | return (0, "0") 1037 | 1038 | def check_length_condition(self, content, condition): 1039 | if '-' in condition: 1040 | start, end = map(int, condition.split('-')) 1041 | return start <= len(content) <= end 1042 | else: 1043 | target_length = int(condition) 1044 | return len(content) == target_length 1045 | 1046 | def check_frequency_condition(self, content, condition): 1047 | conditions = condition.split('|') 1048 | for cond in conditions: 1049 | char, count = cond.split(',') 1050 | if content.count(char) != int(count): 1051 | return False 1052 | return True 1053 | 1054 | 1055 | #======文本组合 1056 | class TextConcatenation: 1057 | @classmethod 1058 | def INPUT_TYPES(cls): 1059 | return { 1060 | "required": { 1061 | "original_text": ("STRING", {"multiline": True, "default": ""}), 1062 | "concatenation_rules": ("STRING", {"multiline": True, "default": ""}), 1063 | "split_char": ("STRING", {"default": ""}), 1064 | }, 1065 | "optional": {}, 1066 | } 1067 | 1068 | RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING") 1069 | FUNCTION = "text_concatenation" 1070 | CATEGORY = "Meeeyo/String" 1071 | DESCRIPTION = note 1072 | def IS_CHANGED(): return float("NaN") 1073 | 1074 | def text_concatenation(self, original_text, concatenation_rules, split_char): 1075 | if split_char: 1076 | original_lines = [line.strip() for line in original_text.split(split_char) if line.strip()] 1077 | else: 1078 | original_lines = [line.strip() for line in original_text.split('\n') if line.strip()] 1079 | 1080 | rules_lines = [line.strip() for line in concatenation_rules.split('\n') if line.strip()] 1081 | 1082 | outputs = [] 1083 | for rule in rules_lines[:5]: 1084 | result = rule 1085 | for i, line in enumerate(original_lines, start=1): 1086 | result = result.replace(f"[{i}]", line) 1087 | outputs.append(result) 1088 | 1089 | while len(outputs) < 5: 1090 | outputs.append("") 1091 | 1092 | return tuple(outputs) 1093 | 1094 | 1095 | #======提取多层指定数据 1096 | class ExtractSpecificData: 1097 | @classmethod 1098 | def INPUT_TYPES(cls): 1099 | return { 1100 | "required": { 1101 | "input_text": ("STRING", {"multiline": True, "default": ""}), 1102 | "rule1": ("STRING", {"default": "[3],@|2"}), 1103 | "rule2": ("STRING", {"default": "三,【|】"}), 1104 | }, 1105 | "optional": {}, 1106 | } 1107 | 1108 | RETURN_TYPES = ("STRING",) 1109 | FUNCTION = "extract_specific_data" 1110 | CATEGORY = "Meeeyo/String" 1111 | DESCRIPTION = note 1112 | def IS_CHANGED(): return float("NaN") 1113 | 1114 | def extract_specific_data(self, input_text, rule1, rule2): 1115 | if rule1.strip(): 1116 | return self.extract_by_rule1(input_text, rule1) 1117 | else: 1118 | return self.extract_by_rule2(input_text, rule2) 1119 | 1120 | def extract_by_rule1(self, input_text, rule): 1121 | try: 1122 | line_rule, split_rule = rule.split(',') 1123 | split_char, group_index = split_rule.split('|') 1124 | group_index = int(group_index) - 1 1125 | except ValueError: 1126 | return ("",) 1127 | 1128 | lines = input_text.split('\n') 1129 | 1130 | if line_rule.startswith('[') and line_rule.endswith(']'): 1131 | try: 1132 | line_index = int(line_rule[1:-1]) - 1 1133 | if 0 <= line_index < len(lines): 1134 | target_line = lines[line_index] 1135 | else: 1136 | return ("",) 1137 | except ValueError: 1138 | return ("",) 1139 | else: 1140 | target_lines = [line for line in lines if line_rule in line] 1141 | if not target_lines: 1142 | return ("",) 1143 | target_line = target_lines[0] 1144 | 1145 | parts = target_line.split(split_char) 1146 | if 0 <= group_index < len(parts): 1147 | return (parts[group_index],) 1148 | return ("",) 1149 | 1150 | def extract_by_rule2(self, input_text, rule): 1151 | try: 1152 | line_rule, tags = rule.split(',') 1153 | start_tag, end_tag = tags.split('|') 1154 | except ValueError: 1155 | return ("",) 1156 | 1157 | lines = input_text.split('\n') 1158 | 1159 | if line_rule.startswith('[') and line_rule.endswith(']'): 1160 | try: 1161 | line_index = int(line_rule[1:-1]) - 1 1162 | if 0 <= line_index < len(lines): 1163 | target_line = lines[line_index] 1164 | else: 1165 | return ("",) 1166 | except ValueError: 1167 | return ("",) 1168 | else: 1169 | target_lines = [line for line in lines if line_rule in line] 1170 | if not target_lines: 1171 | return ("",) 1172 | target_line = target_lines[0] 1173 | 1174 | start_index = target_line.find(start_tag) 1175 | end_index = target_line.find(end_tag, start_index) 1176 | if start_index != -1 and end_index != -1: 1177 | return (target_line[start_index + len(start_tag):end_index],) 1178 | return ("",) 1179 | 1180 | 1181 | #======指定字符行参数 1182 | class FindFirstLineContent: 1183 | @classmethod 1184 | def INPUT_TYPES(cls): 1185 | return { 1186 | "required": { 1187 | "input_text": ("STRING", {"multiline": True, "default": ""}), 1188 | "target_char": ("STRING", {"default": "数据a"}), 1189 | }, 1190 | "optional": {}, 1191 | } 1192 | 1193 | RETURN_TYPES = ("STRING",) 1194 | FUNCTION = "find_first_line_content" 1195 | CATEGORY = "Meeeyo/String" 1196 | DESCRIPTION = note 1197 | def IS_CHANGED(): return float("NaN") 1198 | 1199 | def find_first_line_content(self, input_text, target_char): 1200 | try: 1201 | lines = input_text.splitlines() 1202 | 1203 | for line in lines: 1204 | if target_char in line: 1205 | start_index = line.index(target_char) 1206 | result = line[start_index + len(target_char):] 1207 | return (result,) 1208 | 1209 | return ("",) 1210 | except Exception as e: 1211 | return (f"Error: {str(e)}",) 1212 | 1213 | 1214 | #======获取整数 1215 | class GetIntParam: 1216 | @classmethod 1217 | def INPUT_TYPES(cls): 1218 | return { 1219 | "required": { 1220 | "input_text": ("STRING", {"multiline": False, "default": "", "forceInput": True}), 1221 | "target_char": ("STRING", {"default": ""}), 1222 | }, 1223 | "optional": {}, 1224 | } 1225 | 1226 | RETURN_TYPES = ("INT", "STRING",) 1227 | FUNCTION = "find_first_line_content" 1228 | CATEGORY = "Meeeyo/String" 1229 | DESCRIPTION = note 1230 | def IS_CHANGED(): return float("NaN") 1231 | 1232 | def find_first_line_content(self, input_text, target_char): 1233 | try: 1234 | lines = input_text.splitlines() 1235 | 1236 | for line in lines: 1237 | if target_char in line: 1238 | start_index = line.index(target_char) 1239 | result_str = line[start_index + len(target_char):] 1240 | try: 1241 | result_int = int(result_str) 1242 | except ValueError: 1243 | result_int = None 1244 | 1245 | return (result_int, result_str) 1246 | 1247 | return ("", None) 1248 | 1249 | except Exception as e: 1250 | return (f"Error: {str(e)}", None) 1251 | 1252 | 1253 | #======获取浮点数 1254 | class GetFloatParam: 1255 | @classmethod 1256 | def INPUT_TYPES(cls): 1257 | return { 1258 | "required": { 1259 | "input_text": ("STRING", {"multiline": False, "default": "", "forceInput": True}), 1260 | "target_char": ("STRING", {"default": ""}), 1261 | }, 1262 | "optional": {}, 1263 | } 1264 | 1265 | RETURN_TYPES = ("FLOAT", "STRING",) 1266 | FUNCTION = "find_first_line_content" 1267 | CATEGORY = "Meeeyo/String" 1268 | DESCRIPTION = note 1269 | def IS_CHANGED(): return float("NaN") 1270 | 1271 | def find_first_line_content(self, input_text, target_char): 1272 | try: 1273 | lines = input_text.splitlines() 1274 | 1275 | for line in lines: 1276 | if target_char in line: 1277 | start_index = line.index(target_char) 1278 | result_str = line[start_index + len(target_char):] 1279 | try: 1280 | result_float = float(result_str) 1281 | except ValueError: 1282 | result_float = None 1283 | 1284 | return (result_float, result_str) 1285 | 1286 | return (None, "") 1287 | 1288 | except Exception as e: 1289 | return (None, f"Error: {str(e)}") 1290 | 1291 | 1292 | #======视频指令词模板 1293 | class GenerateVideoPrompt: 1294 | @classmethod 1295 | def INPUT_TYPES(cls): 1296 | return { 1297 | "required": { 1298 | "input_text": ("STRING", {"multiline": True, "default": ""}), 1299 | "mode": (["原文本", "文生视频", "图生视频", "首尾帧视频", "视频负面词"],) 1300 | }, 1301 | "optional": {} 1302 | } 1303 | 1304 | RETURN_TYPES = ("STRING",) 1305 | FUNCTION = "generate_prompt" 1306 | CATEGORY = "Meeeyo/String" 1307 | DESCRIPTION = note 1308 | def IS_CHANGED(): return float("NaN") 1309 | 1310 | def generate_prompt(self, input_text, mode): 1311 | try: 1312 | if mode == "原文本": 1313 | return (input_text,) 1314 | 1315 | elif mode == "文生视频": 1316 | prefix = """You are a highly experienced cinematic director, skilled in creating detailed and engaging visual narratives. When crafting prompts for text-to-video generation based on user input, your goal is to provide precise, chronological descriptions that guide the generation process. Your prompt should focus on clear visual details, including specific movements, appearances, camera angles, and environmental context. 1317 | - Main Action: Start with a clear, concise description of the core action or event in the scene. This should be the focal point of the video. 1318 | - Movement and Gestures: Describe any movements or gestures in the scene, whether from characters, objects, or the environment. Include specifics about how these movements are executed. 1319 | - Appearance of Characters or Objects: Provide detailed descriptions of any characters or objects, focusing on aspects such as their physical appearance, clothing, and visual characteristics. 1320 | - Background and Environment: Elaborate on the surrounding environment, highlighting important visual elements such as landscape features, architecture, or significant objects. These should support the action and enrich the scene. 1321 | - Camera Angles and Movements: Specify the camera perspective (e.g., wide shot, close-up) and any movements (e.g., tracking, zooming, panning). 1322 | - Lighting and Colors: Detail the lighting setup—whether natural, artificial, or dramatic—and how it impacts the scene’s atmosphere. Describe color tones that contribute to the mood. 1323 | - Sudden Changes or Events: If any major shifts occur during the scene (e.g., a lighting change, weather shift, or emotional change), describe these transitions in detail. 1324 | By structuring your prompt in this way, you ensure that the video output will be both engaging and professionally aligned with the user’s intended vision. The description should remain within the 200-word limit while maintaining a smooth flow and cinematic quality. 1325 | The following is the main content of mine: 1326 | """ 1327 | return (prefix + input_text,) 1328 | 1329 | elif mode == "图生视频": 1330 | prefix = """You are tasked with creating a cinematic, highly detailed video scene based on a given image or user description. This prompt is designed to generate an immersive and visually dynamic video experience by focusing on precise, chronological details. The goal is to build a vivid and realistic portrayal of the scene, paying close attention to every element, from the main action to environmental nuances. The description should flow seamlessly, focusing on essential visual and cinematic aspects while adhering to the 200-word limit. 1331 | - Main Action/Focus: 1332 | Begin with a clear, concise description of the central action or key object in the scene. This could be a person, an object, or an event taking place, providing the core of the scene’s narrative. 1333 | - Environment and Objects: 1334 | Describe the surrounding environment or objects in detail. Focus on their textures, colors, scale, and positioning. These details should support the main action and contribute to the atmosphere of the scene. 1335 | - Background Details: 1336 | Provide a vivid depiction of the background. This could include natural or architectural elements, distant landscapes, or other features that add context to the main subject. These details should enrich the visual storytelling. 1337 | - Camera Perspective and Movements: 1338 | Specify the camera angle or perspective being used—whether it’s a wide shot, a close-up, or something more dynamic like a tracking shot or pan. Include any camera movements, such as zooms, tilts, or dollies, if applicable. 1339 | - Lighting and Colors: 1340 | Detail the lighting in the scene, explaining whether it’s natural, artificial, or a combination of both. Consider how the lighting affects the mood, the shadows it creates, and the color temperature (warm or cool). 1341 | - Atmospheric or Environmental Changes: 1342 | If there are any shifts in the scene, like a sudden change in weather, lighting, or emotion, describe these transitions clearly. These environmental changes add dynamic elements to the video. 1343 | - Final Details: 1344 | Ensure that all visual and contextual elements are cohesive and align with the image or input provided. Make sure the description transitions smoothly from one point to the next. 1345 | By following this structure, you ensure that every aspect of the scene is addressed with precision, providing a detailed, cinematic prompt that is easily translated into a video. Keep the descriptions concise, ensuring all visual and environmental factors come together to create a fluid and engaging cinematic experience. 1346 | The following is the main content of mine: 1347 | """ 1348 | return (prefix + input_text,) 1349 | 1350 | elif mode == "首尾帧视频": 1351 | prefix = """You are an expert filmmaker renowned for transforming static imagery into compelling cinematic sequences. Using two images provided by the user, your task is to create a seamless visual narrative that bridges Image One to Image Two. Focus on the dynamic transition, highlighting actions, environmental shifts, and visual elements that unfold in chronological order. Craft your description with the language of cinematography, ensuring a fluid and immersive narrative. 1352 | Requirements: 1353 | Scene Continuity: 1354 | - Begin with a detailed description of Image One’s setting, including central characters, objects, or key visual elements. 1355 | - Follow with a smooth narrative of the transition, emphasizing movement, visual progression, or any changes between the images. 1356 | - Conclude with a description of Image Two’s key details, noting the evolution of the environment, characters, or visual composition. 1357 | Richly Detailed Description: 1358 | - Capture notable actions, expressions, or gestures of characters or subjects. 1359 | - Describe environmental details such as lighting, color palette, weather, and atmosphere. 1360 | - Incorporate cinematographic techniques, including camera angles, zooms, tracking shots, or any dynamic movements. 1361 | Emotional and Contextual Flow: 1362 | - Highlight the emotional connection between the two images or the tone shift (e.g., from calm to tense, or from chaotic to serene). 1363 | - Prioritize visual coherence, even if there are discrepancies between the user’s input and the images. 1364 | Output Format: 1365 | - Begin by detailing Image One’s core elements and actions. 1366 | - Smoothly transition, describing visual progressions and movements. 1367 | - End with the details and conclusion of Image Two. 1368 | - Limit to 200 words in a single cohesive paragraph. 1369 | The following is the main content of mine: 1370 | """ 1371 | return (prefix + input_text,) 1372 | 1373 | elif mode == "视频负面词": 1374 | return ( 1375 | """Overexposure, static artifacts, blurred details, visible subtitles, low-resolution paintings, still imagery, overly gray tones, poor quality, JPEG compression artifacts, unsightly distortions, mutilated features, redundant or extra fingers, poorly rendered hands, poorly depicted faces, anatomical deformities, facial disfigurements, misshapen limbs, fused or distorted fingers, cluttered and distracting background elements, extra or missing limbs (e.g., three legs), overcrowded backgrounds with excessive figures, reversed or upside-down compositions""",) 1376 | 1377 | else: 1378 | return ("",) 1379 | 1380 | except Exception as e: 1381 | return (f"Error: {str(e)}",) 1382 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | ntplib 3 | numpy 4 | torch 5 | Pillow 6 | openpyxl 7 | chardet 8 | pytz --------------------------------------------------------------------------------