Files
2025-12-24 12:41:17 +08:00

914 lines
37 KiB
Python
Raw Permalink 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 json
import os
import hashlib
import time
import torch
import torch.nn.functional as F
"""
最简批量帧文本编码(仅正向),增强兼容性:
- 解决:不同 prompt 生成的 cond 长度不一致导致 torch.cat 报错 -> 统一 padding
- 解决:某些 clip.encode_from_tokens 在 return_pooled=True 时返回 (cond, None) 或根本不支持 pooled 的情况
- 若 pooled 全部为 None,则不放 pooled_output 或放占位空字符串
"""
# ComfyUI 节点注册字典(在文件末尾进行 update 之前,必须先定义)
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
# 参数标签翻译映射
PARAMETER_LABELS = {
"multiline_text": "多行文本",
"use_file": "使用文件",
"file_folder": "提示词文件夹",
"file_name": "文件名",
"mode": "运行模式",
"label": "队列标识",
"preset": "预设模板",
"template": "模板",
"save_preset": "保存预设",
"delete_preset": "删除预设",
"global_prefix": "全局前缀",
"artist_style_prompt": "画师风格提示词",
"prompt_suffix": "提示词后缀",
"preset_name_for_save": "保存预设名称",
"index_setting": "索引设置"
}
def _pq_normalize_prompt_part(s: str) -> str:
"""清理提示词片段首尾空白与多余逗号,避免拼接后出现 ', ,' 或 ',,'。"""
if s is None:
return ""
s = str(s).strip()
# 仅处理首尾逗号(不改中间内容)
s = s.strip(",").strip()
return s
def _pq_build_prompt_file_path(file_folder: str, file_name: str) -> str:
"""
将“提示词文件夹 + 文件名”组合成最终 .txt 路径。
- file_name 若已包含 .txt(忽略大小写),则不重复追加。
- file_folder 可为空;为空时返回 file_name(便于直接写完整路径)。
"""
folder = str(file_folder or "").strip()
name = str(file_name or "").strip()
if not name:
return ""
if not name.lower().endswith(".txt"):
name = name + ".txt"
if folder:
return os.path.join(folder, name)
return name
def parse_frame_prompts(raw_text: str):
"""
解析输入为 {frame_index: prompt} 的升序字典。
允许缺少最外层 {};允许末尾多余逗号。
"""
txt = raw_text.strip()
if not txt:
return {}
if not txt.startswith("{"):
txt = "{" + txt
if not txt.endswith("}"):
txt = txt + "}"
txt = txt.replace(",}", "}")
data = json.loads(txt)
out = {}
for k, v in data.items():
try:
idx = int(k)
except:
continue
# 非数字 key 忽略
out[idx] = str(v)
return dict(sorted(out.items(), key=lambda x: x[0]))
def encode_prompts_sequential(clip, ordered_items):
"""
逐条编码;对 cond 做长度统一;pooled 可能为 None。
ordered_items: List[(frame_index, prompt)]
返回: cond_final, pooled_final(或 None), has_pooled(bool)
"""
cond_list = []
pooled_list = []
pooled_available = True # 先假设有 pooled
max_tokens = 0
with torch.no_grad():
for frame_idx, prompt in ordered_items:
tokens = clip.tokenize(prompt)
# 兼容:有的实现如果 return_pooled=True 仍返回单值
enc = clip.encode_from_tokens(tokens, return_pooled=True)
if isinstance(enc, (list, tuple)) and len(enc) == 2:
cond, pooled = enc
else:
cond, pooled = enc, None
if cond.dim() != 3:
# 期望 shape: [B(=1), T, C]; 若是 [T, C] 补一个 batch 维
if cond.dim() == 2:
cond = cond.unsqueeze(0)
else:
raise ValueError(f"意外的 cond 张量形状: {cond.shape}")
if cond.shape[1] > max_tokens:
max_tokens = cond.shape[1]
cond_list.append(cond)
if pooled is None:
pooled_available = False
pooled_list.append(None)
else:
# 期望 [B(=1), D] 或 [D]; 若 [D] -> [1,D]
if pooled.dim() == 1:
pooled = pooled.unsqueeze(0)
pooled_list.append(pooled)
if not cond_list:
dummy = torch.zeros(1, 1)
# 创建正确维度的 dummy pooled tensor (SDXL 需要 2816 维)
dummy_pooled = torch.zeros(1, 2816)
return dummy, dummy_pooled, False
# 对 cond 做 padding
padded = []
for c in cond_list:
if c.shape[1] < max_tokens:
pad_len = max_tokens - c.shape[1]
# shape [1, T, C],pad 第二维 (tokens):(last_dim_left, last_dim_right, second_dim_left, second_dim_right)
c = F.pad(c, (0, 0, 0, pad_len))
padded.append(c)
cond_final = torch.cat(padded, dim=0) # [N, max_tokens, C]
pooled_final = None
if pooled_available:
# 校验 pooled 维度并 cat
processed = []
for p in pooled_list:
if p is None:
pooled_available = False
break
if p.dim() == 1:
p = p.unsqueeze(0)
processed.append(p)
if pooled_available and processed:
try:
pooled_final = torch.cat(processed, dim=0) # [N, D]
except Exception:
# 如果维度不一致,放弃 pooled
pooled_final = None
pooled_available = False
# 如果 pooled 不可用,创建 dummy pooled tensor
if pooled_final is None:
# 创建正确维度的 dummy pooled tensor (SDXL 需要 2816 维)
pooled_final = torch.zeros(cond_final.shape[0], 2816) # [N, 2816]
return cond_final, pooled_final, pooled_available
def _read_lines_from_source(multiline_text: str, file_path: str = "", use_file: bool = False):
"""
从多行文本或文件读取,每行一个提示词;自动去除空行和首尾空白。
"""
lines = []
if use_file and file_path:
try:
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
lines = f.readlines()
except Exception:
# 回退到多行文本
lines = multiline_text.splitlines()
else:
lines = multiline_text.splitlines()
cleaned = []
for raw in lines:
s = str(raw).strip()
if not s:
continue
cleaned.append(s)
return cleaned
def _apply_template_to_lines(lines, template: str):
"""
将每行替换进模板。若模板为空或不含 {p},则直接返回行本身。
"""
if not template or "{p}" not in template:
return lines
return [template.replace("{p}", line) for line in lines]
def _parse_index_setting(index_str: str) -> list:
"""
解析索引设置字符串。
支持格式:
- "-1" 或 "": 记忆模式 (Auto/Resume) -> 返回 [-1]
- "0": 重置模式 (Reset to 0) -> 返回 [0]
- "5": 单行模式 -> 返回 [5] (第6行)
- "2,8,35": 列表模式 -> 返回 [2, 8, 35]
"""
s = str(index_str).strip()
if not s:
return [-1]
# 尝试分割逗号
parts = s.replace(",", ",").split(",")
indices = []
for p in parts:
p = p.strip()
if not p:
continue
try:
val = int(p)
indices.append(val)
except ValueError:
continue
if not indices:
return [-1]
return indices
class NegativePromptQueue:
"""
负面提示词队列节点,专门用于处理负面提示词。
支持多行文本输入和预设模板功能,模板配置方式与PromptQueue一致。
"""
@classmethod
def INPUT_TYPES(cls):
# 动态获取预设列表(负面)
presets = _pq_load_negative_template_presets()
preset_list = ["None"] + list(presets.keys())
return {
"required": {
"clip": ("CLIP",),
"多行文本": ("STRING", {"multiline": True, "default": "blurry, low quality, distorted"}),
"预设模板": (preset_list, {"default": "None"}),
"模板": ("STRING", {"multiline": False, "default": "{p}"}),
"保存预设": ("BOOLEAN", {"default": False}),
"删除预设": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = ("CONDITIONING",)
RETURN_NAMES = ("negative",)
FUNCTION = "run"
CATEGORY = "PromptQueue"
def run(self, clip, 多行文本, 预设模板="None", 模板="{p}", 保存预设=False, 删除预设=False):
# 处理删除预设 - 拥有最高优先级
if 删除预设 and 预设模板 != "None":
_pq_delete_negative_template_preset(预设模板)
print(f"[Negative Prompt Queue] 已删除预设 '{预设模板}'")
print(f"[Negative Prompt Queue] 使用 template 内容(删除预设后): {模板}")
# 处理保存预设 - 第二优先级,强制使用 template 内容
elif 保存预设 and 模板.strip():
_pq_save_negative_template_preset(模板.strip(), 模板.strip())
print(f"[Negative Prompt Queue] 已保存预设 '{模板.strip()}': {模板.strip()}")
print(f"[Negative Prompt Queue] 使用 template 内容(save_preset 优先级): {模板}")
# 处理预设选择 - 当选择非"None"预设且未开启保存/删除时,使用预设内容
elif 预设模板 != "None":
presets = _pq_load_negative_template_presets()
if 预设模板 in presets:
模板 = presets[预设模板]
print(f"[Negative Prompt Queue] 使用预设 '{预设模板}': {模板}")
else:
print(f"[Negative Prompt Queue] 预设 '{预设模板}' 不存在,使用 template 内容")
else:
print(f"[Negative Prompt Queue] 使用 template 内容: {模板}")
lines = _read_lines_from_source(多行文本, "", False)
lines = _apply_template_to_lines(lines, 模板)
if not lines:
# 使用标准的 CLIP 编码方式处理空输入
tokens = clip.tokenize("")
cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True)
if pooled is None:
pooled = torch.zeros(1, 2816) # SDXL 需要 2816 维
return ([[cond, {"pooled_output": pooled}]],)
# 使用标准的 CLIP 编码方式处理每一行
cond_list = []
pooled_list = []
with torch.no_grad():
for line in lines:
tokens = clip.tokenize(line)
cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True)
# 确保 cond 是 3D 张量 [B, T, C]
if cond.dim() == 2:
cond = cond.unsqueeze(0)
# 确保 pooled 是 2D 张量 [B, D]
if pooled is None:
pooled = torch.zeros(1, 2816) # SDXL 需要 2816 维
elif pooled.dim() == 1:
pooled = pooled.unsqueeze(0)
cond_list.append(cond)
pooled_list.append(pooled)
# 将所有条件连接起来
if cond_list:
# 对 cond 做 padding 以确保长度一致
max_tokens = max(c.shape[1] for c in cond_list)
padded_conds = []
for c in cond_list:
if c.shape[1] < max_tokens:
pad_len = max_tokens - c.shape[1]
c = F.pad(c, (0, 0, 0, pad_len))
padded_conds.append(c)
cond_final = torch.cat(padded_conds, dim=0)
pooled_final = torch.cat(pooled_list, dim=0)
else:
# 空情况
cond_final = torch.zeros(1, 1)
pooled_final = torch.zeros(1, 2816)
return ([[cond_final, {"pooled_output": pooled_final}]],)
@classmethod
def IS_CHANGED(cls, clip, 多行文本, 预设模板="None", 模板="{p}", 保存预设=False, 删除预设=False):
# 当预设更改时强制重新执行以实时更新模板
if 预设模板 != "None":
return str(time.time())
# 对于确定性模式,对输入进行哈希以便缓存按预期工作
try:
desc = _pq_source_descriptor(False, "", str(多行文本))
key = f"{desc}|{模板}|{预设模板}"
return hashlib.sha1(key.encode("utf-8", errors="ignore")).hexdigest()
except Exception:
return ""
class StylePromptQueue:
"""
[增量模式] 风格化提示词队列节点。
- 每次运行仅输出一行提示词。
- 即使 Batch Size > 1,也全部使用这一行(用于提高容错率)。
- 索引设置:支持 "-1"(记忆/自动), "0"(从0开始), "5"(从5开始), "2,8,35"(列表循环)
"""
@classmethod
def INPUT_TYPES(cls):
# 动态获取预设列表(正面)
presets = _pq_load_positive_template_presets()
preset_list = ["None"] + list(presets.keys())
return {
"required": {
"clip": ("CLIP",),
"全局前缀": ("STRING", {"multiline": False, "default": ""}),
"画师风格提示词": ("STRING", {"multiline": False, "default": ""}),
"多行文本": ("STRING", {"multiline": True, "default": "A cat\nA dog"}),
"提示词后缀": ("STRING", {"multiline": False, "default": ""}),
"使用文件": ("BOOLEAN", {"default": False}),
"提示词文件夹": ("STRING", {"multiline": False, "default": ""}),
"文件名": ("STRING", {"multiline": False, "default": ""}),
"索引设置": ("STRING", {"multiline": False, "default": "-1"}),
"预设模板": (preset_list, {"default": "None"}),
"保存预设名称": ("STRING", {"multiline": False, "default": ""}),
"保存预设": ("BOOLEAN", {"default": False}),
"删除预设": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = ("CONDITIONING", "STRING")
RETURN_NAMES = ("positive", "prompt_text")
FUNCTION = "run"
CATEGORY = "PromptQueue"
def run(self, clip, 全局前缀, 画师风格提示词, 多行文本, 提示词后缀, 使用文件, 提示词文件夹, 文件名, 索引设置, 预设模板, 保存预设名称, 保存预设, 删除预设, unique_id=None):
# 全局前缀:仅当输入不为空时生效;留空则完全等同于旧逻辑
global_prefix = _pq_normalize_prompt_part(全局前缀)
prefix = 画师风格提示词
suffix = 提示词后缀
# 处理预设
if 删除预设 and 预设模板 != "None":
_pq_delete_positive_template_preset(预设模板)
print(f"[Style Prompt Queue] 已删除预设 '{预设模板}'")
elif 保存预设 and 保存预设名称.strip():
preset_name = 保存预设名称.strip()
parts = []
p = prefix.strip()
s = suffix.strip()
if p:
parts.append(p)
parts.append("{p}")
if s:
parts.append(s)
preset_data = ",".join(parts)
_pq_save_positive_template_preset(preset_name, preset_data)
print(f"[Style Prompt Queue] 已保存预设 '{preset_name}': {preset_data}")
elif 预设模板 != "None":
presets = _pq_load_positive_template_presets()
if 预设模板 in presets:
preset_content = presets[预设模板]
is_legacy_json = False
try:
data = json.loads(preset_content)
if isinstance(data, dict) and ("prefix" in data or "suffix" in data):
prefix = data.get("prefix", "")
suffix = data.get("suffix", "")
print(f"[Style Prompt Queue] 已从旧版JSON格式加载预设 '{预设模板}'")
is_legacy_json = True
except (json.JSONDecodeError, TypeError):
pass
if not is_legacy_json:
if "{p}" in preset_content:
parts = preset_content.split("{p}", 1)
prefix = parts[0].rstrip(',').strip()
if len(parts) > 1:
suffix = parts[1].lstrip(',').strip()
else:
suffix = ""
print(f"[Style Prompt Queue] 已加载预设 '{预设模板}'")
else:
prefix = preset_content
suffix = ""
print(f"[Style Prompt Queue] 无法解析预设 '{预设模板}',已作为前缀处理")
# 组合文件路径(优先“文件夹+文件名”,回退到“文件路径”以兼容旧工作流)
effective_file_path = _pq_build_prompt_file_path(提示词文件夹, 文件名)
lines = _read_lines_from_source(多行文本, effective_file_path, 使用文件)
# 应用前缀和后缀
final_lines = []
for line in lines:
parts = []
for p in [global_prefix, prefix, line, suffix]:
pp = _pq_normalize_prompt_part(p)
if pp:
parts.append(pp)
final_lines.append(", ".join(parts))
if not final_lines:
dummy = torch.zeros(1, 1)
dummy_pooled = torch.zeros(1, 2816)
return ([[dummy, {"pooled_output": dummy_pooled}]], "")
# --- 核心索引控制逻辑 ---
indices = _parse_index_setting(索引设置)
idx_mode = indices[0]
target_idx = 0
desc = _pq_source_descriptor(使用文件, effective_file_path, 多行文本)
# 扩展逻辑:支持 "Start From X" 且自动递增
# 检测 索引设置变更 或 源内容变更
queue_label = "Auto_Queue"
if unique_id:
queue_label = f"{unique_id}_Auto_Queue"
current_setting_str = str(索引设置).strip()
# 索引设置状态也需要隔离,否则不同节点使用相同文本但不同设置时会互相干扰
setting_key = f"{queue_label}_Settings_{desc}"
last_setting = _pq_store.get("Index Settings", setting_key, "")
setting_changed = (current_setting_str != last_setting)
if setting_changed:
_pq_store.put("Index Settings", setting_key, current_setting_str)
# 获取 Auto_Queue 的最后一次源,用于判断是否发生源变更
last_source = _pq_store.get("PromptQueue Sources", queue_label, None)
source_changed = (last_source != desc)
should_reset_counter = setting_changed or source_changed
if idx_mode == -1:
# 记忆模式 (Auto/Resume)
target_idx = _pq_get_next_index(queue_label, len(final_lines), desc)
elif len(indices) == 1 and idx_mode >= 0:
# 0: Auto from beginning (Start from 0 + Auto Increment)
# N > 0: Fixed single line N (1-based index) -> Index N-1
if idx_mode == 0:
# Auto Mode (0)
if should_reset_counter:
_pq_store.put("PromptQueue Counters", queue_label, 0)
_pq_store.put("PromptQueue Sources", queue_label, desc)
target_idx = _pq_get_next_index(queue_label, len(final_lines), desc)
else:
# Fixed Line Mode (e.g. 1 -> 1st line -> index 0)
# Non-incremental
target_idx = idx_mode - 1
else:
# 列表模式 (2,8,35) -> 视为 1-based index
# 使用一个基于列表内容的特殊 key
list_key = f"List_{索引设置}"
# 这里的 total 是列表长度
list_idx = _pq_get_next_index(list_key, len(indices), list_key)
val = indices[list_idx]
# 转换为 0-based
target_idx = val - 1
if target_idx < 0:
target_idx = 0
# 越界保护
if target_idx >= len(final_lines):
target_idx = target_idx % len(final_lines)
if target_idx < 0:
target_idx = 0
# Debug print
print(f"[PromptQueue] Index Setting: '{索引设置}' -> Mode: {idx_mode} -> Target Line: {target_idx + 1}")
line_to_process = final_lines[target_idx]
ordered = [(0, line_to_process)]
cond, pooled, has_pooled = encode_prompts_sequential(clip, ordered)
meta = {"pooled_output": pooled}
return ([[cond, meta]], line_to_process)
@classmethod
def IS_CHANGED(cls, clip, 全局前缀, 画师风格提示词, 多行文本, 提示词后缀, 使用文件, 提示词文件夹, 文件名, 索引设置, 预设模板, 保存预设名称, 保存预设, 删除预设):
# 始终返回时间戳以强制重新执行
return str(time.time())
class SimpleStylePromptQueue(StylePromptQueue):
"""
[增量模式] 简化版提示词队列节点 (无预设管理)。
继承自 StylePromptQueue,但去除了预设管理相关的输入。
"""
@classmethod
def INPUT_TYPES(cls):
presets = _pq_load_positive_template_presets()
preset_list = ["None"] + list(presets.keys())
return {
"required": {
"clip": ("CLIP",),
"全局前缀": ("STRING", {"multiline": False, "default": ""}),
"画师风格提示词": ("STRING", {"multiline": False, "default": ""}),
"多行文本": ("STRING", {"multiline": True, "default": "A cat\nA dog"}),
"提示词后缀": ("STRING", {"multiline": False, "default": ""}),
"使用文件": ("BOOLEAN", {"default": False}),
"提示词文件夹": ("STRING", {"multiline": False, "default": ""}),
"文件名": ("STRING", {"multiline": False, "default": ""}),
"索引设置": ("STRING", {"multiline": False, "default": "-1"}),
"预设模板": (preset_list, {"default": "None"}),
}
}
def run(self, clip, 全局前缀, 画师风格提示词, 多行文本, 提示词后缀, 使用文件, 提示词文件夹, 文件名, 索引设置, 预设模板, unique_id=None):
# 调用父类的 run,传入 None/False 作为预设参数
return super().run(
clip,
全局前缀,
画师风格提示词,
多行文本,
提示词后缀,
使用文件,
提示词文件夹,
文件名,
索引设置,
预设模板,
"",
False,
False,
unique_id=unique_id
)
@classmethod
def IS_CHANGED(cls, clip, 全局前缀, 画师风格提示词, 多行文本, 提示词后缀, 使用文件, 提示词文件夹, 文件名, 索引设置, 预设模板):
return str(time.time())
class StylePromptQueueBatch:
"""
[批量模式] 风格化提示词队列节点。
- 每次运行输出所有行(或指定范围)。
- 这里的索引设置主要用于指定 "从哪一行开始" 或 "只输出哪几行"。
"""
@classmethod
def INPUT_TYPES(cls):
presets = _pq_load_positive_template_presets()
preset_list = ["None"] + list(presets.keys())
return {
"required": {
"clip": ("CLIP",),
"全局前缀": ("STRING", {"multiline": False, "default": ""}),
"画师风格提示词": ("STRING", {"multiline": False, "default": ""}),
"多行文本": ("STRING", {"multiline": True, "default": "A cat\nA dog"}),
"提示词后缀": ("STRING", {"multiline": False, "default": ""}),
"使用文件": ("BOOLEAN", {"default": False}),
"提示词文件夹": ("STRING", {"multiline": False, "default": ""}),
"文件名": ("STRING", {"multiline": False, "default": ""}),
"索引设置": ("STRING", {"multiline": False, "default": "0"}),
"预设模板": (preset_list, {"default": "None"}),
"保存预设名称": ("STRING", {"multiline": False, "default": ""}),
"保存预设": ("BOOLEAN", {"default": False}),
"删除预设": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = ("CONDITIONING", "STRING")
RETURN_NAMES = ("positive", "prompt_text_list")
FUNCTION = "run"
CATEGORY = "PromptQueue"
def run(self, clip, 全局前缀, 画师风格提示词, 多行文本, 提示词后缀, 使用文件, 提示词文件夹, 文件名, 索引设置, 预设模板, 保存预设名称, 保存预设, 删除预设):
# 全局前缀:仅当输入不为空时生效;留空则完全等同于旧逻辑
global_prefix = _pq_normalize_prompt_part(全局前缀)
prefix = 画师风格提示词
suffix = 提示词后缀
# 处理预设
if 删除预设 and 预设模板 != "None":
_pq_delete_positive_template_preset(预设模板)
print(f"[Prompt Queue Batch] 已删除预设 '{预设模板}'")
elif 保存预设 and 保存预设名称.strip():
preset_name = 保存预设名称.strip()
parts = []
p = prefix.strip()
s = suffix.strip()
if p:
parts.append(p)
parts.append("{p}")
if s:
parts.append(s)
preset_data = ",".join(parts)
_pq_save_positive_template_preset(preset_name, preset_data)
print(f"[Prompt Queue Batch] 已保存预设 '{preset_name}': {preset_data}")
elif 预设模板 != "None":
presets = _pq_load_positive_template_presets()
if 预设模板 in presets:
preset_content = presets[预设模板]
# 简化处理,仅支持新版 {p} 格式或纯前缀
if "{p}" in preset_content:
parts = preset_content.split("{p}", 1)
prefix = parts[0].rstrip(',').strip()
if len(parts) > 1:
suffix = parts[1].lstrip(',').strip()
else:
suffix = ""
else:
prefix = preset_content
suffix = ""
effective_file_path = _pq_build_prompt_file_path(提示词文件夹, 文件名)
lines = _read_lines_from_source(多行文本, effective_file_path, 使用文件)
final_lines = []
for line in lines:
parts = []
for p in [global_prefix, prefix, line, suffix]:
pp = _pq_normalize_prompt_part(p)
if pp:
parts.append(pp)
final_lines.append(", ".join(parts))
if not final_lines:
dummy = torch.zeros(1, 1)
dummy_pooled = torch.zeros(1, 2816)
return ([[dummy, {"pooled_output": dummy_pooled}]], "")
# 索引处理
# 批量模式下,索引设置通常意味着:
# "0": 输出所有,从 0 开始
# "5": 输出所有,从 5 开始 (5, 6, 7...)
# "2,8,35": 只输出这几行
indices = _parse_index_setting(索引设置)
selected_lines = []
if indices[0] == -1:
# 默认全部
selected_lines = list(enumerate(final_lines))
elif len(indices) == 1 and indices[0] >= 0:
# 从指定行开始到结束
start_idx = indices[0]
if start_idx >= len(final_lines):
start_idx = 0 # 越界回退
# 创建切片
subset = final_lines[start_idx:]
# 重新编号,为了 encode_prompts_sequential 里的排序(其实顺序已经定了)
# 注意:这里的 idx 仅仅用于排序,只要是升序即可
for i, line in enumerate(subset):
selected_lines.append((i, line))
else:
# 指定列表
for i, idx in enumerate(indices):
if 0 <= idx < len(final_lines):
selected_lines.append((i, final_lines[idx]))
if not selected_lines:
# 如果选完是空的,回退到全部
selected_lines = list(enumerate(final_lines))
cond, pooled, has_pooled = encode_prompts_sequential(clip, selected_lines)
meta = {"pooled_output": pooled}
# 批量模式下返回所有选中的提示词文本,用换行符分隔
output_text = "\n".join([item[1] for item in selected_lines])
return ([[cond, meta]], output_text)
@classmethod
def IS_CHANGED(cls, **kwargs):
# 批量模式通常是确定性的,除非输入变了
# 但为了避免缓存导致修改索引无效,我们可以简单 hash 输入
return float("nan") # 总是更新
class SimpleStylePromptQueueBatch(StylePromptQueueBatch):
"""
[批量模式] 简化版提示词队列节点 (无预设管理)。
继承自 StylePromptQueueBatch,但去除了预设管理相关的输入。
"""
@classmethod
def INPUT_TYPES(cls):
presets = _pq_load_positive_template_presets()
preset_list = ["None"] + list(presets.keys())
return {
"required": {
"clip": ("CLIP",),
"全局前缀": ("STRING", {"multiline": False, "default": ""}),
"画师风格提示词": ("STRING", {"multiline": False, "default": ""}),
"多行文本": ("STRING", {"multiline": True, "default": "A cat\nA dog"}),
"提示词后缀": ("STRING", {"multiline": False, "default": ""}),
"使用文件": ("BOOLEAN", {"default": False}),
"提示词文件夹": ("STRING", {"multiline": False, "default": ""}),
"文件名": ("STRING", {"multiline": False, "default": ""}),
"索引设置": ("STRING", {"multiline": False, "default": "0"}),
"预设模板": (preset_list, {"default": "None"}),
}
}
def run(self, clip, 全局前缀, 画师风格提示词, 多行文本, 提示词后缀, 使用文件, 提示词文件夹, 文件名, 索引设置, 预设模板):
# 调用父类的 run,传入 None 作为预设参数
return super().run(clip, 全局前缀, 画师风格提示词, 多行文本, 提示词后缀, 使用文件, 提示词文件夹, 文件名, 索引设置, 预设模板, "", False, False)
@classmethod
def IS_CHANGED(cls, clip, 全局前缀, 画师风格提示词, 多行文本, 提示词后缀, 使用文件, 提示词文件夹, 文件名, 索引设置, 预设模板):
return float("nan")
# --- 持久队列状态(每个标签)---
class _JsonKVStore:
def __init__(self, file_path: str):
self.file_path = file_path
self.data = {}
self._load()
def _load(self):
try:
if os.path.exists(self.file_path):
with open(self.file_path, "r", encoding="utf-8") as f:
self.data = json.load(f)
else:
self.data = {}
except Exception:
self.data = {}
def _save(self):
try:
os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump(self.data, f, indent=2)
except Exception:
pass
def get(self, category: str, key: str, default=None):
return self.data.get(category, {}).get(key, default)
def put(self, category: str, key: str, value) -> None:
if category not in self.data:
self.data[category] = {}
self.data[category][key] = value
self._save()
def remove(self, category: str, key: str) -> None:
"""从存储中移除一个键"""
if category in self.data and key in self.data[category]:
del self.data[category][key]
self._save()
_pq_state_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_prompt_queue_state")
_pq_store = _JsonKVStore(os.path.join(_pq_state_dir, "state.json"))
_preset_store = _JsonKVStore(os.path.join(_pq_state_dir, "presets.json"))
# --- 数据迁移逻辑 ---
# 检查旧 store 中是否有预设数据,如果有则迁移到新 store 并从旧 store 删除
_migration_occurred = False
for category in ["Template Presets Positive", "Template Presets Negative"]:
if category in _pq_store.data:
# 复制数据到 preset store
if category not in _preset_store.data:
_preset_store.data[category] = {}
# 合并数据(优先保留 preset store 现有的,或者覆盖?这里假设 preset store 为空或应该包含旧数据)
for k, v in _pq_store.data[category].items():
_preset_store.data[category][k] = v
# 从旧 store 删除 category
del _pq_store.data[category]
_migration_occurred = True
if _migration_occurred:
_preset_store._save()
_pq_store._save()
print("[PromptQueue] Migrated presets from state.json to presets.json")
def _pq_source_descriptor(use_file: bool, file_path: str, multiline_text: str) -> str:
if use_file and file_path:
try:
return "FILE:" + os.path.abspath(file_path)
except Exception:
return "FILE:" + file_path
# 文本哈希作为描述符;内容更改不会重置索引以允许在追加时继续
h = hashlib.sha1(multiline_text.encode("utf-8", errors="ignore")).hexdigest()
return "TEXT:" + h
def _pq_get_next_index(label: str, _pq_total: int, source_descriptor: str) -> int:
if _pq_total <= 0:
return 0
# 仅在源类型在 FILE 和 TEXT 之间切换或文件路径更改时重置
stored_src = _pq_store.get("PromptQueue Sources", label, None)
if stored_src != source_descriptor:
_pq_store.put("PromptQueue Sources", label, source_descriptor)
_pq_store.put("PromptQueue Counters", label, 0)
current = _pq_store.get("PromptQueue Counters", label, 0) or 0
next_index = current % _pq_total
_pq_store.put("PromptQueue Counters", label, (current + 1) % _pq_total)
return next_index
def _pq_save_positive_template_preset(name: str, template: str) -> None:
"""保存正面模板预设"""
_preset_store.put("Template Presets Positive", name, template)
def _pq_load_positive_template_presets() -> dict:
"""加载所有正面模板预设"""
return _preset_store.data.get("Template Presets Positive", {})
def _pq_delete_positive_template_preset(name: str) -> None:
"""删除正面模板预设"""
_preset_store.remove("Template Presets Positive", name)
def _pq_save_negative_template_preset(name: str, template: str) -> None:
"""保存负面模板预设"""
_preset_store.put("Template Presets Negative", name, template)
def _pq_load_negative_template_presets() -> dict:
"""加载所有负面模板预设"""
return _preset_store.data.get("Template Presets Negative", {})
def _pq_delete_negative_template_preset(name: str) -> None:
"""删除负面模板预设"""
_preset_store.remove("Template Presets Negative", name)
# 注册新节点
NODE_CLASS_MAPPINGS.update({
"NegativePromptQueue": NegativePromptQueue,
"StylePromptQueue": StylePromptQueue,
"SimpleStylePromptQueue": SimpleStylePromptQueue,
"StylePromptQueueBatch": StylePromptQueueBatch,
"SimpleStylePromptQueueBatch": SimpleStylePromptQueueBatch,
})
NODE_DISPLAY_NAME_MAPPINGS.update({
"NegativePromptQueue": "Negative PQ / 负面队列",
"StylePromptQueue": "PQ (Incremental) / 增量队列",
"SimpleStylePromptQueue": "PQ Simple (Incr.) / 增量简易",
"StylePromptQueueBatch": "PQ (Batch) / 批量队列",
"SimpleStylePromptQueueBatch": "PQ Simple (Batch) / 批量简易",
})