更新 DynamicTagLoaderJS 類別,優化檔案讀取邏輯,讀取子資料夾的功能

This commit is contained in:
NineKey
2026-01-09 18:25:40 +08:00
parent 651f032507
commit 2ebceecc53
2 changed files with 92 additions and 22 deletions
+44 -18
View File
@@ -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
View File
@@ -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)