更新 DynamicTagLoaderJS 類別,優化檔案讀取邏輯,讀取子資料夾的功能
This commit is contained in:
+44
-18
@@ -2,9 +2,11 @@ import os
|
||||
import folder_paths
|
||||
from .loader_node import DynamicTagLoaderJS
|
||||
from .saver_node import DynamicTagSaver
|
||||
from .iterator_node import DynamicTagIterator # [新增] 匯入新節點
|
||||
from .iterator_node import DynamicTagIterator
|
||||
|
||||
# 嘗試導入 ComfyUI 伺服器模組
|
||||
# ==============================================================================
|
||||
# 模組導入與環境檢查
|
||||
# ==============================================================================
|
||||
try:
|
||||
from server import PromptServer
|
||||
from aiohttp import web
|
||||
@@ -12,51 +14,75 @@ except ImportError:
|
||||
PromptServer = None
|
||||
web = None
|
||||
|
||||
# 初始化路徑配置:確保 tags 目錄存在
|
||||
# ==============================================================================
|
||||
# 全域路徑配置
|
||||
# ==============================================================================
|
||||
NODE_FILE_PATH = os.path.dirname(os.path.abspath(__file__))
|
||||
TAGS_DIR = os.path.join(NODE_FILE_PATH, "tags")
|
||||
|
||||
# 若 tags 目錄不存在則自動建立,確保基本執行環境
|
||||
if not os.path.exists(TAGS_DIR):
|
||||
os.makedirs(TAGS_DIR)
|
||||
|
||||
# 註冊 API 路由
|
||||
# ==============================================================================
|
||||
# API 路由註冊 (Server-Side)
|
||||
# ==============================================================================
|
||||
if PromptServer:
|
||||
|
||||
# API: 讀取 tags 資料夾結構與 .txt 檔案列表
|
||||
@PromptServer.instance.routes.get("/custom_nodes/tags")
|
||||
async def get_tags_data(request):
|
||||
"""
|
||||
API: 獲取 Tags 目錄結構
|
||||
功能: 遞迴遍歷 tags 資料夾,回傳包含 .txt 檔案的目錄結構供前端選單使用。
|
||||
"""
|
||||
data = {}
|
||||
if os.path.exists(TAGS_DIR):
|
||||
# 取得所有子目錄
|
||||
subdirs = sorted([d for d in os.listdir(TAGS_DIR) if os.path.isdir(os.path.join(TAGS_DIR, d))])
|
||||
for subdir in subdirs:
|
||||
subdir_path = os.path.join(TAGS_DIR, subdir)
|
||||
# 取得子目錄下的 .txt 檔案
|
||||
files = sorted([f for f in os.listdir(subdir_path) if f.endswith(".txt")])
|
||||
data[subdir] = ["ALL"] + files
|
||||
# 使用 os.walk 進行遞迴遍歷,以支援多層級子資料夾
|
||||
for root, dirs, files in os.walk(TAGS_DIR):
|
||||
# 篩選出目標檔案類型 (.txt)
|
||||
txt_files = sorted([f for f in files if f.endswith(".txt")])
|
||||
|
||||
# 過濾空目錄:僅將包含有效 .txt 檔案的目錄加入索引
|
||||
if txt_files:
|
||||
# 計算相對路徑 (例如: "Style/Anime")
|
||||
rel_path = os.path.relpath(root, TAGS_DIR)
|
||||
|
||||
# 根目錄標識處理
|
||||
if rel_path == ".":
|
||||
rel_path = "Root"
|
||||
|
||||
# 跨平台相容性處理:統一使用 POSIX 風格路徑分隔符 (/) 以確保前端顯示一致
|
||||
rel_path = rel_path.replace("\\", "/")
|
||||
|
||||
# 建構回傳資料:加入 "ALL" 選項作為批次讀取標識
|
||||
data[rel_path] = ["ALL"] + txt_files
|
||||
|
||||
return web.json_response(data)
|
||||
|
||||
# API: 獲取 ComfyUI 系統內可用的 LoRA 列表 (供前端 JS 使用)
|
||||
@PromptServer.instance.routes.get("/custom_nodes/loras_list")
|
||||
async def get_loras_list(request):
|
||||
"""
|
||||
API: 獲取系統 LoRA 列表
|
||||
功能: 讀取 ComfyUI 系統路徑下的 LoRA 模型清單。
|
||||
"""
|
||||
loras = folder_paths.get_filename_list("loras")
|
||||
return web.json_response(loras)
|
||||
|
||||
# 節點類別映射
|
||||
# ==============================================================================
|
||||
# 節點映射與顯示名稱
|
||||
# ==============================================================================
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"DynamicTagLoaderJS": DynamicTagLoaderJS,
|
||||
"DynamicTagSaver": DynamicTagSaver,
|
||||
"DynamicTagIterator": DynamicTagIterator # [新增] 註冊類別
|
||||
"DynamicTagIterator": DynamicTagIterator
|
||||
}
|
||||
|
||||
# 節點顯示名稱映射
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"DynamicTagLoaderJS": "⚡Dynamic Tag Loader",
|
||||
"DynamicTagSaver": "💾 Dynamic Tag Saver",
|
||||
"DynamicTagIterator": "🔄 Dynamic Tag Iterator" # [新增] 顯示名稱
|
||||
"DynamicTagIterator": "🔄 Dynamic Tag Iterator"
|
||||
}
|
||||
|
||||
# 前端資源目錄
|
||||
WEB_DIRECTORY = "./web"
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
+48
-4
@@ -6,10 +6,17 @@ import folder_paths
|
||||
import comfy.sd
|
||||
import comfy.utils
|
||||
|
||||
# 定義基礎路徑常數
|
||||
NODE_FILE_PATH = os.path.dirname(os.path.abspath(__file__))
|
||||
TAGS_DIR = os.path.join(NODE_FILE_PATH, "tags")
|
||||
|
||||
class DynamicTagLoaderJS:
|
||||
"""
|
||||
ComfyUI 自定義節點:動態 Tag 載入器
|
||||
功能:根據前端傳入的 JSON 設定,動態讀取資料夾內的 Prompt 檔案,
|
||||
並支援 LoRA 的自動解析與載入。
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
@@ -23,19 +30,21 @@ class DynamicTagLoaderJS:
|
||||
}
|
||||
}
|
||||
|
||||
# [修改] 新增一個 INT 類型的輸出: count
|
||||
# 定義輸出類型與名稱
|
||||
RETURN_TYPES = ("MODEL", "CLIP", "CONDITIONING", "STRING", "INT")
|
||||
RETURN_NAMES = ("model", "clip", "positive", "prompt", "count")
|
||||
OUTPUT_IS_LIST = (True, True, True, True, False) # count 不是 list,是單一數值
|
||||
OUTPUT_IS_LIST = (True, True, True, True, False) # 注意: count 為單一數值,非列表
|
||||
|
||||
FUNCTION = "process"
|
||||
CATEGORY = "Custom/TagLoader"
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(s, **kwargs):
|
||||
# 設定為 NaN 以確保每次執行時都會重新計算 (避免快取導致不更新)
|
||||
return float("nan")
|
||||
|
||||
def _parse_and_strip_lora(self, text):
|
||||
"""解析並分離文本中的 <lora:...> 語法,回傳純淨文本與 LoRA 設定列表"""
|
||||
if not text:
|
||||
return "", []
|
||||
lora_pattern = r"<lora:([^>:]+)(?::([0-9.]+))?>"
|
||||
@@ -44,17 +53,24 @@ class DynamicTagLoaderJS:
|
||||
lora_name = match.group(1)
|
||||
strength = float(match.group(2)) if match.group(2) else 1.0
|
||||
found_loras.append((lora_name, strength))
|
||||
|
||||
# 清除 LoRA 標籤並規範化換行符號
|
||||
cleaned_text = re.sub(lora_pattern, "", text)
|
||||
cleaned_text = re.sub(r'\n\s*\n', '\n', cleaned_text)
|
||||
cleaned_text = cleaned_text.strip()
|
||||
return cleaned_text, found_loras
|
||||
|
||||
def _load_lora(self, model, clip, lora_name, strength_model, strength_clip):
|
||||
"""動態載入指定的 LoRA 模型並應用於 Model 與 CLIP"""
|
||||
if model is None or clip is None:
|
||||
return model, clip
|
||||
|
||||
# 嘗試搜尋 LoRA 檔案路徑
|
||||
lora_path = folder_paths.get_full_path("loras", lora_name)
|
||||
if lora_path is None:
|
||||
lora_path = folder_paths.get_full_path("loras", f"{lora_name}.safetensors")
|
||||
|
||||
# 若完全匹配失敗,嘗試模糊搜尋 (忽略副檔名大小寫)
|
||||
if lora_path is None:
|
||||
available_loras = folder_paths.get_filename_list("loras")
|
||||
target_name = lora_name.lower()
|
||||
@@ -65,9 +81,11 @@ class DynamicTagLoaderJS:
|
||||
if candidate_name == target_name:
|
||||
lora_path = folder_paths.get_full_path("loras", candidate)
|
||||
break
|
||||
|
||||
if lora_path is None:
|
||||
print(f"[DynamicTagLoader] Warning: Lora not found: {lora_name}")
|
||||
return model, clip
|
||||
|
||||
try:
|
||||
lora_model = comfy.utils.load_torch_file(lora_path, safe_load=True)
|
||||
model_lora, clip_lora = comfy.sd.load_lora_for_models(model, clip, lora_model, strength_model, strength_clip)
|
||||
@@ -77,6 +95,7 @@ class DynamicTagLoaderJS:
|
||||
return model, clip
|
||||
|
||||
def _read_file(self, path):
|
||||
"""安全讀取檔案內容"""
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
return f.read().strip()
|
||||
@@ -84,6 +103,13 @@ class DynamicTagLoaderJS:
|
||||
return None
|
||||
|
||||
def process(self, text_input, tag_settings, model=None, clip=None, **kwargs):
|
||||
"""
|
||||
核心處理函數
|
||||
1. 解析 tag_settings JSON 設定
|
||||
2. 讀取指定資料夾/檔案的 Prompt
|
||||
3. 組合 Prompt 並解析其中的 LoRA
|
||||
4. 生成最終的 Conditioning 與 Model 輸出
|
||||
"""
|
||||
delimiter = "\n"
|
||||
try:
|
||||
settings = json.loads(tag_settings)
|
||||
@@ -92,6 +118,8 @@ class DynamicTagLoaderJS:
|
||||
|
||||
base_text_cleaned, base_loras = self._parse_and_strip_lora(text_input)
|
||||
prompts_groups = []
|
||||
|
||||
# 依照前端 UI 順序排序 (key 為索引值)
|
||||
sorted_keys = sorted(settings.keys(), key=lambda x: int(x))
|
||||
|
||||
for key in sorted_keys:
|
||||
@@ -99,24 +127,36 @@ class DynamicTagLoaderJS:
|
||||
item_type = item.get("type", "file")
|
||||
|
||||
if item_type == "text":
|
||||
# 處理純文字輸入
|
||||
raw_text = item.get("text", "")
|
||||
if raw_text:
|
||||
cleaned_text, loras = self._parse_and_strip_lora(raw_text)
|
||||
prompts_groups.append([(cleaned_text, loras)])
|
||||
else:
|
||||
# 處理檔案讀取
|
||||
folder_name = item.get("folder")
|
||||
file_name = item.get("file")
|
||||
if not folder_name or not file_name:
|
||||
continue
|
||||
folder_path = os.path.join(TAGS_DIR, folder_name)
|
||||
|
||||
# 路徑解析:處理根目錄標識與相對路徑轉換 (相容多層級子目錄)
|
||||
if folder_name == "Root":
|
||||
folder_path = TAGS_DIR
|
||||
else:
|
||||
folder_path = os.path.join(TAGS_DIR, os.path.normpath(folder_name))
|
||||
|
||||
current_group_data = []
|
||||
files_to_read = []
|
||||
|
||||
# 檔案選取策略
|
||||
if file_name == "ALL":
|
||||
if os.path.exists(folder_path):
|
||||
# 僅列出當前目錄下的 .txt 檔案 (不包含子目錄內容,符合邏輯設計)
|
||||
files_to_read = sorted([f for f in os.listdir(folder_path) if f.endswith(".txt")])
|
||||
else:
|
||||
files_to_read = [file_name]
|
||||
|
||||
# 讀取檔案內容
|
||||
for f_name in files_to_read:
|
||||
raw_content = self._read_file(os.path.join(folder_path, f_name))
|
||||
if raw_content is not None:
|
||||
@@ -126,6 +166,7 @@ class DynamicTagLoaderJS:
|
||||
if current_group_data:
|
||||
prompts_groups.append(current_group_data)
|
||||
|
||||
# 產生所有組合 (笛卡兒積)
|
||||
if not prompts_groups:
|
||||
combinations = [([],)] if base_text_cleaned or base_loras else []
|
||||
else:
|
||||
@@ -136,6 +177,7 @@ class DynamicTagLoaderJS:
|
||||
final_prompts = []
|
||||
final_conditionings = []
|
||||
|
||||
# 遍歷組合並構建輸出
|
||||
for combo in combinations:
|
||||
current_texts = []
|
||||
if base_text_cleaned:
|
||||
@@ -143,6 +185,7 @@ class DynamicTagLoaderJS:
|
||||
current_texts.extend([item[0] for item in combo if item and item[0]])
|
||||
combined_prompt = delimiter.join(current_texts)
|
||||
|
||||
# 合併全域與局部 LoRA
|
||||
all_loras = []
|
||||
if base_loras:
|
||||
all_loras.extend(base_loras)
|
||||
@@ -150,12 +193,14 @@ class DynamicTagLoaderJS:
|
||||
if item:
|
||||
all_loras.extend(item[1])
|
||||
|
||||
# 應用 LoRA 至 Model 與 CLIP
|
||||
current_model = model
|
||||
current_clip = clip
|
||||
if current_model is not None and current_clip is not None and all_loras:
|
||||
for lora_name, strength in all_loras:
|
||||
current_model, current_clip = self._load_lora(current_model, current_clip, lora_name, strength, strength)
|
||||
|
||||
# CLIP Tokenize 與 Encode
|
||||
current_conditioning = None
|
||||
if current_clip is not None:
|
||||
try:
|
||||
@@ -176,5 +221,4 @@ class DynamicTagLoaderJS:
|
||||
if not final_prompts:
|
||||
return ([], [], [], [], 0)
|
||||
|
||||
# [修改] 回傳值多了一個 count
|
||||
return (final_models, final_clips, final_conditionings, final_prompts, count)
|
||||
Reference in New Issue
Block a user