Files
aidec-Comfyui_TextBatch_aidec/modules/textbatch/nodes.py
T
2025-10-04 14:51:06 +08:00

970 lines
37 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import json
import logging
from server import PromptServer
import torch
import numpy as np
from typing import List, Union
import glob
from PIL import Image
from PIL import ImageOps
import comfy
import folder_paths
import base64
from io import BytesIO
# 設定基本的日誌記錄格式和級別
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class TextBatchNode:
"""
文本批次處理節點
用於將大型文本文件按照指定分隔符分割成多個部分
支持狀態保存和恢復,可以記住上次處理的位置
"""
def __init__(self):
# 初始化狀態文件路徑,用於保存處理進度
self.state_file = os.path.join(os.path.dirname(__file__), "text_batch_state.json")
self.state = self.load_state()
self.reset_state() # 添加初始化重置
def reset_state(self):
"""重置狀態到初始值"""
self.state = {
"prompts": [],
"current_index": 0,
"last_input": "",
"last_input_mode": "",
"last_separator": "",
"last_separator_type": "newline",
"last_start_index": 0,
"completed": False
}
self.save_state()
@classmethod
def INPUT_TYPES(cls):
"""
定義節點的輸入參數類型和預設值
"""
return {
"required": {
# 修改預設值為 text
"input_mode": (["text", "file"], {"default": "text"}),
# 文本文件路徑輸入
"text_file": ("STRING", {"multiline": False, "default": "Enter the path to your text file here"}),
# 直接輸入文本
"input_text": ("STRING", {
"multiline": True,
"default": "Enter your text here...",
"placeholder": "Enter multiple prompts, one per line"
}),
# 分隔符類型
"separator_type": (["newline", "custom"], {"default": "newline"}),
# 文本分隔符
"separator": ("STRING", {"default": "---"}),
# 起始索引位置
"start_index": ("INT", {"default": 0, "min": 0, "max": 10000}),
# 新增自動終止選項
"auto_stop": ("BOOLEAN", {"default": True}),
},
"hidden": {"unique_id": "UNIQUE_ID"}
}
# 定義輸出類型
RETURN_TYPES = ("STRING", "STRING", "INT", "INT", "BOOLEAN")
# 定義輸出名稱
RETURN_NAMES = ("prompt", "status", "current_index", "total", "completed")
FUNCTION = "process_text"
CATEGORY = "TextBatch"
OUTPUT_NODE = True # 添加這行
def process_text(self, input_mode, text_file, input_text, separator_type, separator, start_index, auto_stop, unique_id=None):
"""
處理文本文件或直接輸入文本的主要方法
參數:
input_mode: 輸入模式(file或text)
text_file: 要處理的文本文件路徑
input_text: 直接輸入的文本
separator_type: 分隔符類型(custom或newline)
separator: 用於分割文本的分隔符
start_index: 開始處理的索引位置
auto_stop: 是否自動終止
unique_id: 節點的唯一識別碼,用於更新節點顯示
返回:
tuple: (提示文本, 狀態信息, 當前索引, 總數, 是否完成)
"""
try:
# 檔案模式的驗證
if input_mode == "file":
if not text_file.strip() or text_file == "Enter the path to your text file here":
return ("", "Error: Please provide a valid file path", -1, 0, True)
if not os.path.exists(text_file):
return ("", f"Error: File not found: {text_file}", -1, 0, True)
with open(text_file, 'r', encoding='utf-8') as file:
content = file.read().strip()
if not content:
return ("", "Error: File is empty", -1, 0, True)
current_input = text_file
else:
if not input_text.strip() or input_text == "Enter your text here...":
return ("", "Error: Please provide input text", -1, 0, True)
current_input = input_text
# 檢查是否需要重置
need_reset = (
self.state.get("last_input") != current_input or
self.state.get("last_input_mode") != input_mode or
self.state.get("last_separator") != separator or
self.state.get("last_separator_type") != separator_type or
self.state.get("completed", False)
)
# 如果需要重置,重新加載所有數據
if need_reset:
self.reset_state()
if input_mode == "file":
self.load_prompts(text_file, separator_type, separator)
else:
self.load_text_input(input_text, separator_type, separator)
# 檢查是否成功加載了提示
if len(self.state["prompts"]) == 0:
return ("", "Error: No valid prompts found", -1, 0, True)
self.state.update({
"last_input": current_input,
"last_input_mode": input_mode,
"last_separator": separator,
"last_separator_type": separator_type,
"current_index": 0
})
total = len(self.state["prompts"])
if total == 0:
return ("", "No prompts loaded", 0, 0, True)
# 安全獲取當前索引
current_index = min(self.state.get("current_index", 0), total - 1)
try:
# 安全獲取當前提示
prompt = self.state["prompts"][current_index]
except IndexError:
# 如果發生索引錯誤,重置到最後一個有效索引
current_index = total - 1
prompt = self.state["prompts"][current_index]
self.state["current_index"] = current_index
# 檢查是否完成
is_last = current_index >= total - 1
# 更新狀態
if not is_last and auto_stop:
self.state["current_index"] = current_index + 1
completed = False
else:
completed = True
self.state["completed"] = completed
# 生成狀態信息
status = f"Processing {current_index + 1}/{total}"
if input_mode == "file":
status += f" | File: {os.path.basename(text_file)}"
if completed:
status += " | Completed"
# 保存狀態
self.save_state()
# 更新節點顯示的當前索引
if not completed:
PromptServer.instance.send_sync("textbatch-node-feedback",
{"node_id": unique_id, "widget_name": "start_index", "type": "int", "value": self.state["current_index"]})
return (prompt, status, current_index, total, completed)
except Exception as e:
logger.error(f"Error in process_text: {str(e)}")
return ("", f"Error: {str(e)}", -1, 0, True)
def load_prompts(self, text_file, separator_type, separator):
"""
從文件中加載並分割提示文本
參數:
text_file: 文本文件路徑
separator_type: 分隔符類型(custom或newline)
separator: 用於分割文本的分隔符
"""
try:
with open(text_file, 'r', encoding='utf-8') as file:
content = file.read()
# 根據分隔符類型選擇分割方式
if separator_type == "newline":
self.state["prompts"] = [prompt.strip() for prompt in content.splitlines() if prompt.strip()]
else:
self.state["prompts"] = [prompt.strip() for prompt in content.split(separator) if prompt.strip()]
logger.info(f"Loaded {len(self.state['prompts'])} prompts from {text_file}")
except Exception as e:
logger.error(f"Error loading prompts: {str(e)}")
raise
def load_text_input(self, input_text, separator_type, separator):
"""
處理直接輸入的文本
參數:
input_text: 輸入的文本
separator_type: 分隔符類型(custom或newline)
separator: 用於分割文本的分隔符
"""
try:
# 根據分隔符類型選擇分割方式
if separator_type == "newline":
self.state["prompts"] = [prompt.strip() for prompt in input_text.splitlines() if prompt.strip()]
else:
self.state["prompts"] = [prompt.strip() for prompt in input_text.split(separator) if prompt.strip()]
logger.info(f"Loaded {len(self.state['prompts'])} prompts from direct input")
except Exception as e:
logger.error(f"Error processing input text: {str(e)}")
raise
def load_state(self):
"""
從狀態文件中加載之前的處理狀態
"""
try:
if os.path.exists(self.state_file):
with open(self.state_file, 'r') as f:
return json.load(f)
except Exception as e:
logger.error(f"Error loading state file: {str(e)}")
return {
"prompts": [],
"current_index": 0,
"last_input": "",
"last_input_mode": "file",
"last_separator": "",
"last_separator_type": "newline",
"last_start_index": 0,
"completed": False # 添加完成狀態標記
}
def save_state(self):
"""
將當前處理狀態保存到狀態文件
"""
try:
with open(self.state_file, 'w') as f:
json.dump(self.state, f)
except Exception as e:
logger.error(f"Error saving state file: {str(e)}")
@classmethod
def IS_CHANGED(cls, **kwargs):
"""
控制節點的執行頻率
返回不同的值會觸發節點重新執行
"""
current_index = kwargs.get("current_index", 0)
total = kwargs.get("total", 0)
completed = kwargs.get("completed", False)
auto_stop = kwargs.get("auto_stop", True)
# 如果啟用了自動停止且未完成,返回 float("nan") 觸發重新執行
if auto_stop and not completed and current_index < total - 1:
return float("nan") # 使用 nan 確保每次都會觸發重新執行
return current_index # 返回當前索引,不會觸發重新執行
class TextSplitCounterNode:
"""
文本分割計數節點
用於計算分割後的文本總數
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input_mode": (["file", "text"], {"default": "file"}),
"text_file": ("STRING", {"multiline": False, "default": "Enter the path to your text file here"}),
"input_text": ("STRING", {
"multiline": True,
"default": "Enter your text here...",
}),
"separator_type": (["newline", "custom"], {"default": "newline"}),
"separator": ("STRING", {"default": "---"}),
}
}
RETURN_TYPES = ("INT", "STRING",)
RETURN_NAMES = ("count", "status")
FUNCTION = "count_splits"
CATEGORY = "TextBatch"
def count_splits(self, input_mode, text_file, input_text, separator_type, separator):
try:
if input_mode == "file":
if not os.path.exists(text_file):
return (0, f"Error: File not found: {text_file}")
with open(text_file, 'r', encoding='utf-8') as file:
content = file.read()
else:
content = input_text
# 根據分隔符類型計算分割數
if separator_type == "newline":
splits = [x.strip() for x in content.splitlines() if x.strip()]
else:
splits = [x.strip() for x in content.split(separator) if x.strip()]
count = len(splits)
status = f"Total splits: {count}"
return (count, status)
except Exception as e:
logger.error(f"Error in count_splits: {str(e)}")
return (0, f"Error: {str(e)}")
class TextQueueProcessor:
"""處理文字佇列的節點"""
def __init__(self):
self.state_file = os.path.join(os.path.dirname(__file__), "text_queue_processor_state.json")
self.state = self.load_state()
self.reset_state()
def reset_state(self):
"""重置狀態到初始值"""
self.state = {
"current_index": 0,
"last_input": "",
"completed": False
}
self.save_state()
def load_state(self):
"""從狀態文件中加載之前的處理狀態"""
try:
if os.path.exists(self.state_file):
with open(self.state_file, 'r') as f:
return json.load(f)
except Exception as e:
logger.error(f"Error loading state file: {str(e)}")
return {
"current_index": 0,
"last_input": "",
"completed": False
}
def save_state(self):
"""將當前處理狀態保存到狀態文件"""
try:
with open(self.state_file, 'w') as f:
json.dump(self.state, f)
except Exception as e:
logger.error(f"Error saving state file: {str(e)}")
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {
"multiline": True,
"default": "1girl\n1cat\n1dog",
"placeholder": "輸入提示詞,可用分隔符或換行分割"
}),
"separator_type": (["newline", "custom"], {"default": "newline"}),
"separator": ("STRING", {"default": ","}),
"start_index": ("INT", {"default": 0, "min": 0, "max": 10000}),
"trigger_next": ("BOOLEAN", {"default": True, "label_on": "Trigger", "label_off": "Don't trigger"}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO"
}
}
RETURN_TYPES = ("STRING", "INT", "INT", "BOOLEAN", "STRING")
RETURN_NAMES = ("text", "current_index", "total", "completed", "status")
FUNCTION = "process"
CATEGORY = "TextBatch"
OUTPUT_NODE = True
def process(self, text, separator_type, separator, start_index, trigger_next, unique_id, prompt=None, extra_pnginfo=None):
try:
# 檢查是否需要重置
need_reset = (
self.state.get("last_input") != text or
self.state.get("completed", False) or
(prompt and extra_pnginfo)
)
if need_reset:
self.reset_state()
self.state["last_input"] = text
# 根據分隔符類型分割文本
if separator_type == "newline":
lines = [line.strip() for line in text.splitlines() if line.strip()]
else:
lines = [line.strip() for line in text.split(separator) if line.strip()]
total = len(lines)
if total == 0:
return ("", -1, 0, True, "No valid text found")
# 獲取當前索引
current_index = min(max(start_index, self.state.get("current_index", 0)), total - 1)
# 獲取當前行
current_text = lines[current_index]
# 檢查是否是最後一行
is_last = current_index >= total - 1
# 更新狀態
if not is_last and trigger_next:
self.state["current_index"] = current_index + 1
completed = False
# 使用自己的事件名稱
PromptServer.instance.send_sync("textbatch-add-queue", {})
else:
completed = True
self.state["current_index"] = 0
self.state["completed"] = completed
self.save_state()
# 生成狀態信息
status = f"Processing {current_index + 1}/{total}"
if completed:
status += " | Completed"
# 更新節點顯示的當前索引
if not completed:
PromptServer.instance.send_sync("textbatch-node-feedback",
{"node_id": unique_id, "widget_name": "start_index", "type": "int", "value": self.state["current_index"]})
return (current_text, current_index, total, completed, status)
except Exception as e:
logger.error(f"Error in process: {str(e)}")
return ("", -1, 0, True, f"Error: {str(e)}")
class ImageQueueProcessor:
"""處理圖片佇列的節點"""
def __init__(self):
self.state_file = os.path.join(os.path.dirname(__file__), "image_queue_processor_state.json")
self.state = self.load_state()
self.reset_state()
def reset_state(self):
self.state = {
"current_index": 0,
"last_input": "",
"completed": False
}
self.save_state()
def load_state(self):
try:
if os.path.exists(self.state_file):
with open(self.state_file, 'r') as f:
return json.load(f)
except Exception as e:
logger.error(f"Error loading state file: {str(e)}")
return {
"current_index": 0,
"last_input": "",
"completed": False
}
def save_state(self):
try:
with open(self.state_file, 'w') as f:
json.dump(self.state, f)
except Exception as e:
logger.error(f"Error saving state file: {str(e)}")
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",), # 接受多張圖片的輸入
"start_index": ("INT", {"default": 0, "min": 0, "max": 10000}),
"trigger_next": ("BOOLEAN", {"default": True, "label_on": "Trigger", "label_off": "Don't trigger"}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO"
}
}
RETURN_TYPES = ("IMAGE", "INT", "INT", "BOOLEAN", "STRING")
RETURN_NAMES = ("image", "current_index", "total", "completed", "status")
FUNCTION = "process"
CATEGORY = "TextBatch"
OUTPUT_NODE = True
def process(self, images, start_index, trigger_next, unique_id, prompt=None, extra_pnginfo=None):
try:
# 確保輸入是 tensor 並且格式正確
if not isinstance(images, torch.Tensor):
return (None, -1, 0, True, "Invalid input: not a tensor")
# 處理單張圖片的情況
if len(images.shape) == 3:
images = images.unsqueeze(0)
# 獲取總數
total = images.shape[0]
if total == 0:
return (None, -1, 0, True, "No images found")
# 生成唯一的輸入標識符
input_hash = str(hash(str(images.shape)))
# 檢查是否需要重置
need_reset = (
self.state.get("last_input") != input_hash or
self.state.get("completed", False) or
(prompt and extra_pnginfo)
)
if need_reset:
self.reset_state()
self.state["last_input"] = input_hash
current_index = start_index
else:
current_index = min(max(start_index, self.state.get("current_index", 0)), total - 1)
# 獲取當前圖片
current_image = images[current_index:current_index+1]
# 檢查是否是最後一張
is_last = current_index >= total - 1
# 更新狀態
if not is_last and trigger_next:
next_index = current_index + 1
self.state["current_index"] = next_index
completed = False
# 只有在非最後一張且啟用 trigger_next 時才發送佇列事件
if next_index < total:
PromptServer.instance.send_sync("textbatch-add-queue", {})
else:
completed = True
self.state["current_index"] = 0
self.state["completed"] = True
self.state["completed"] = completed
self.save_state()
# 生成狀態信息
status = f"Processing {current_index + 1}/{total}"
if completed:
status += " | Completed"
# 更新節點顯示的當前索引
if not completed:
PromptServer.instance.send_sync("textbatch-node-feedback",
{"node_id": unique_id, "widget_name": "start_index", "type": "int", "value": self.state["current_index"]})
return (current_image, current_index, total, completed, status)
except Exception as e:
logger.error(f"Error in process: {str(e)}")
return (None, -1, 0, True, f"Error: {str(e)}")
class ImageInfoExtractorNode:
"""圖片資訊提取節點
用於提取圖片的基本資訊,包括尺寸、檔名等
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",), # 接受圖片輸入
"image_path": ("STRING", {
"multiline": False,
"default": "",
"placeholder": "可選:輸入圖片路徑以獲取更多資訊"
}),
}
}
RETURN_TYPES = ("INT", "INT", "INT", "INT", "STRING", "STRING", "STRING", "STRING", "STRING")
RETURN_NAMES = ("width", "height", "batch_size", "channels", "file_name", "file_format", "file_size", "color_mode", "status")
FUNCTION = "extract_info"
CATEGORY = "TextBatch"
def extract_info(self, images, image_path=""):
try:
# 確保輸入是 tensor 並且格式正確
if not isinstance(images, torch.Tensor):
return (0, 0, 0, 0, "", "", "", "", "錯誤:輸入不是有效的圖片張量")
# 處理單張圖片的情況
if len(images.shape) == 3:
images = images.unsqueeze(0)
# 獲取基本資訊
batch_size = images.shape[0]
height = images.shape[1]
width = images.shape[2]
channels = images.shape[3]
# 預設值
file_name = ""
file_format = ""
file_size = ""
color_mode = ""
# 如果提供了圖片路徑,嘗試獲取額外資訊
if image_path and os.path.exists(image_path):
try:
file_size = f"{os.path.getsize(image_path) / (1024 * 1024):.2f}MB"
file_name = os.path.basename(image_path)
# 使用 PIL 讀取圖片以獲取更多資訊
with Image.open(image_path) as img:
color_mode = img.mode
file_format = img.format or os.path.splitext(image_path)[1]
except Exception as e:
return (width, height, batch_size, channels, "", "", "", "", f"讀取檔案資訊時發生錯誤: {str(e)}")
status = f"成功提取 {batch_size} 張圖片的資訊"
return (width, height, batch_size, channels, file_name, file_format, file_size, color_mode, status)
except Exception as e:
logger.error(f"提取圖片資訊時發生錯誤: {str(e)}")
return (0, 0, 0, 0, "", "", "", "", f"錯誤: {str(e)}")
class PathParserNode:
"""路徑解析節點
用於解析檔案路徑,分離出檔名和資料夾路徑
支援絕對路徑和相對路徑
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"file_path": ("STRING", {
"multiline": False,
"default": "",
"placeholder": "輸入完整檔案路徑"
}),
"normalize_path": ("BOOLEAN", {
"default": True,
"label_on": "是",
"label_off": "否"
}),
}
}
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING")
RETURN_NAMES = ("file_name_no_ext", "folder_path", "extension", "absolute_path", "status")
FUNCTION = "parse_path"
CATEGORY = "TextBatch"
def parse_path(self, file_path: str, normalize_path: bool = True):
try:
if not file_path:
return ("", "", "", "", "錯誤:未提供檔案路徑")
# 檢查輸入類型並轉換為字串
if isinstance(file_path, (list, tuple)):
# 如果是列表或元組,取第一個元素
if len(file_path) > 0:
file_path = str(file_path[0])
else:
return ("", "", "", "", "錯誤:空列表")
else:
file_path = str(file_path)
# 標準化路徑分隔符
file_path = os.path.normpath(file_path)
# 轉換為絕對路徑
absolute_path = os.path.abspath(file_path)
# 取得檔案名稱(含副檔名)和資料夾路徑
folder_path = os.path.dirname(absolute_path)
full_filename = os.path.basename(absolute_path)
# 分離檔名和副檔名
file_name_no_ext, extension = os.path.splitext(full_filename)
# 如果副檔名存在,移除開頭的點
extension = extension[1:] if extension.startswith('.') else extension
# 處理路徑格式
if normalize_path:
# 使用正斜線
folder_path = folder_path.replace('\\', '/')
absolute_path = absolute_path.replace('\\', '/')
# 生成狀態信息
status_parts = []
status_parts.append(f"檔名: {file_name_no_ext}")
if extension:
status_parts.append(f"副檔名: {extension}")
status_parts.append(f"目錄: {folder_path}")
status = " | ".join(status_parts)
return (
file_name_no_ext, # 無副檔名的檔名
folder_path, # 資料夾路徑
extension, # 副檔名(不含點號)
absolute_path, # 完整絕對路徑
status # 狀態信息
)
except Exception as e:
logger.error(f"解析路徑時發生錯誤: {str(e)}")
return ("", "", "", "", f"錯誤: {str(e)}")
class LoadImagesFromDirBatchM:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"directory": ("STRING", {"default": ""}),
},
"optional": {
"image_load_cap": ("INT", {"default": 0, "min": 0, "step": 1}),
"start_index": ("INT", {"default": 0, "min": -1, "step": 1}),
"load_always": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
}
}
RETURN_TYPES = ("IMAGE", "MASK", "INT", "STRING", "STRING", "STRING")
RETURN_NAMES = ("image", "mask", "count", "filenames", "full_paths", "path_only")
FUNCTION = "load_images"
CATEGORY = "image"
@classmethod
def IS_CHANGED(cls, **kwargs):
if 'load_always' in kwargs and kwargs['load_always']:
return float("NaN")
else:
return hash(frozenset(kwargs))
def load_images(self, directory: str, image_load_cap: int = 0, start_index: int = 0, load_always=False):
if not os.path.isdir(directory):
raise FileNotFoundError(f"Directory '{directory} cannot be found.'")
dir_files = os.listdir(directory)
if len(dir_files) == 0:
raise FileNotFoundError(f"No files in directory '{directory}'.")
# Filter files by extension
valid_extensions = ['.jpg', '.jpeg', '.png', '.webp']
dir_files = [f for f in dir_files if any(f.lower().endswith(ext) for ext in valid_extensions)]
dir_files = sorted(dir_files)
dir_files = [os.path.join(directory, x) for x in dir_files]
# start at start_index
dir_files = dir_files[start_index:]
images = []
masks = []
filenames = [] # 檔名列表(不含路徑)
full_paths = [] # 完整路徑列表
path_only = directory # =輸入路徑
limit_images = False
if image_load_cap > 0:
limit_images = True
image_count = 0
has_non_empty_mask = False
for image_path in dir_files:
if os.path.isdir(image_path):
continue
if limit_images and image_count >= image_load_cap:
break
i = Image.open(image_path)
i = ImageOps.exif_transpose(i)
image = i.convert("RGB")
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
if 'A' in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
has_non_empty_mask = True
else:
mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu")
images.append(image)
masks.append(mask)
# 儲存檔名(不含路徑)
filenames.append(os.path.basename(image_path))
# 儲存標準化的完整路徑
norm_path = os.path.abspath(image_path).replace('\\', '/')
full_paths.append(norm_path)
image_count += 1
if len(images) == 1:
# 單張圖片時返回單一檔名和路徑
return (images[0], masks[0], 1, filenames[0], full_paths[0], path_only)
elif len(images) > 1:
image1 = images[0]
mask1 = None
for image2 in images[1:]:
if image1.shape[1:] != image2.shape[1:]:
image2 = comfy.utils.common_upscale(image2.movedim(-1, 1), image1.shape[2], image1.shape[1], "bilinear", "center").movedim(1, -1)
image1 = torch.cat((image1, image2), dim=0)
for mask2 in masks:
if has_non_empty_mask:
if image1.shape[1:3] != mask2.shape:
mask2 = torch.nn.functional.interpolate(mask2.unsqueeze(0).unsqueeze(0), size=(image1.shape[1], image1.shape[2]), mode='bilinear', align_corners=False)
mask2 = mask2.squeeze(0)
else:
mask2 = mask2.unsqueeze(0)
else:
mask2 = mask2.unsqueeze(0)
if mask1 is None:
mask1 = mask2
else:
mask1 = torch.cat((mask1, mask2), dim=0)
# 多張圖片時返回以逗號分隔的檔名和路徑字串
return (image1, mask1, len(images), ",".join(filenames), ",".join(full_paths), path_only)
class ImageFilenameProcessor:
"""圖片檔名處理節點
用於處理單張或多張圖片的檔名,可以根據索引獲取特定檔名
提供完整檔名、無副檔名、副檔名和完整路徑等多種輸出
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"filenames": ("STRING", {
"multiline": False,
"default": "",
"placeholder": "逗號分隔的檔名或路徑列表"
}),
"index": ("INT", {
"default": 0,
"min": 0,
"step": 1,
"display": "number"
}),
"directory": ("STRING", {
"multiline": False,
"default": "",
"placeholder": "可選:指定檔案目錄路徑"
}),
}
}
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "INT", "STRING")
RETURN_NAMES = ("filename", "name_no_ext", "extension", "full_path", "total_files", "status")
FUNCTION = "process_filename"
CATEGORY = "TextBatch"
def process_filename(self, filenames: str, index: int = 0, directory: str = ""):
try:
# 處理空輸入
if not filenames.strip():
return ("", "", "", "", 0, "錯誤:沒有輸入檔名")
# 分割檔名列表
if '\n' in filenames:
path_list = [f.strip() for f in filenames.split("\n") if f.strip()]
else:
path_list = [f.strip() for f in filenames.split(",") if f.strip()]
total_files = len(path_list)
# 處理空列表
if total_files == 0:
return ("", "", "", "", 0, "錯誤:沒有有效的檔名")
# 確保索引在有效範圍內
if index < 0:
index = 0
if index >= total_files:
index = total_files - 1
# 獲取指定索引的路徑
selected_path = path_list[index]
# 從路徑中提取檔名
filename = os.path.basename(selected_path)
# 分離檔名和副檔名
name_no_ext, extension = os.path.splitext(filename)
# 確保副檔名不包含點號
extension = extension[1:] if extension.startswith('.') else extension
# 處理完整路徑
if directory:
# 如果提供了目錄,使用該目錄和檔名組合
directory = os.path.normpath(directory)
full_path = os.path.join(directory, filename)
else:
# 否則使用輸入的路徑
full_path = selected_path
# 標準化路徑格式(使用正斜線)
full_path = os.path.normpath(full_path).replace('\\', '/')
# 生成狀態信息
status = f"成功獲取第 {index + 1}/{total_files} 個檔名"
if directory:
status += f" (目錄: {directory})"
return (
filename, # 完整檔名(含副檔名)
name_no_ext, # 無副檔名
extension, # 副檔名(不含點號)
full_path, # 完整路徑
total_files, # 總檔案數
status # 狀態信息
)
except Exception as e:
logger.error(f"處理檔名時發生錯誤: {str(e)}")
return ("", "", "", "", 0, f"錯誤: {str(e)}")
# 節點類映射
NODE_CLASS_MAPPINGS = {
"TextBatch": TextBatchNode,
"TextQueueProcessor": TextQueueProcessor,
"TextSplitCounter": TextSplitCounterNode,
"ImageQueueProcessor": ImageQueueProcessor,
"ImageInfoExtractor": ImageInfoExtractorNode,
"PathParser": PathParserNode,
"LoadImagesFromDirBatch": LoadImagesFromDirBatchM,
"ImageFilenameProcessor": ImageFilenameProcessor # 添加新節點
}
# 節點顯示名稱映射
NODE_DISPLAY_NAME_MAPPINGS = {
"TextBatch": "Text Batch",
"TextQueueProcessor": "Text Queue Processor",
"TextSplitCounter": "Text Split Counter",
"ImageQueueProcessor": "Image Queue Processor",
"ImageInfoExtractor": "Image Info Extractor",
"PathParser": "Path Parser",
"LoadImagesFromDirBatch": "Load Images From Dir Batch",
"ImageFilenameProcessor": "Image Filename Processor" # 添加新節點
}