7 Commits
Author SHA1 Message Date
Cyan9977 0b86452a6e Add files via upload 2025-12-23 19:39:38 +08:00
Cyan9977 fbbe3d2e60 Add files via upload 2025-12-10 09:45:27 +08:00
Cyan9977 28cb7cfec3 Add files via upload 2025-12-09 16:40:09 +08:00
Cyan9977 09c4f12d94 Add files via upload 2025-12-09 16:16:05 +08:00
Cyan9977 9780a9d720 Add files via upload 2025-12-09 15:22:13 +08:00
Cyan9977 618de1ce1a Add files via upload 2025-12-09 15:00:41 +08:00
Cyan9977 97c958ffb4 Add files via upload
1.  节点拆分: 将原有的 `Style Prompt Queue` 拆分为两个独立的节点:
    `PQ (Incremental) / 增量队列`: 专注于单次/逐行生成,支持强大的索引控制。
    `PQ (Batch) / 批量队列`: 专注于一次性批量输出。
2.  预设系统优化:
    功能增强: 批量版 (`PQ Batch`) 现已支持完整的预设保存与删除功能。
    简易版升级: 简易版 (`PQ Simple`) 现已增加预设选择功能,可直接调用已有预设。
    文件分离: 预设数据现已独立存储于 `_prompt_queue_state/presets.json`,方便分享与备份;运行状态隔离在 `state.json`。
    ⚠️ 注意: 保存或删除预设后,必须运行一次工作流以执行操作,然后刷新浏览器页面,新的预设列表才会显示在下拉菜单中。
3.  节点重命名: 采用更紧凑的中英命名(如 `PQ (Incremental) / 增量队列`),显著缩短节点宽度,提升 UI 体验。
4.  移除过时节点: 移除了旧版的 `PromptQueue` 和 `SimplePromptQueue`,建议全面迁移到新版节点。
5.  全新的索引控制: 引入了 `索引设置` 参数,替代了旧版的队列标识逻辑,支持更灵活的行选择(重置、锁定、列表循环)。
6.  UI 优化: 调整了参数顺序,更符合操作直觉:前缀 -> 主体 -> 后缀。
7.  汉化: 全面汉化了节点参数和选项。
2025-12-09 14:59:33 +08:00
4 changed files with 558 additions and 284 deletions
+89 -69
View File
@@ -4,48 +4,78 @@
---
## 节点介绍
## 📢 更新日志 (Update Log)
### 1. Style Prompt Queue / 风格化提示词队列 🆕
### 2025-12-09 重要更新:功能拆分与逻辑重构
为了提供更清晰的使用体验,本次更新对核心功能进行了重构:
这是一个功能强大的节点,专门用于**序列化生成**。它将“画师/风格”部分和“通用后缀”从核心提示词中分离出来,让你能够轻松地对一个主体列表应用不同的风格组合。
- **核心功能**:
- **画师/风格提示词**: 在此输入艺术家、画风等前缀。
- **提示词序列**: 在此输入你的核心主体,每行一个,例如“一只猫”、“一条狗”。
- **提示词后缀**: 在此输入通用后缀,例如“杰作, 8k”。
- **预设系统**: 将常用的“画师/风格”和“后缀”组合保存为预设,方便一键切换。
- **文件支持**: 可以通过外部 `.txt` 文件加载提示词序列。
- **运行模式**:
- **固定为增量模式**: 每次执行只处理序列中的一个提示词,非常适合长序列的自动化生成。
### 2. Prompt Queue / 提示词队列
一个通用的正向提示词队列节点,支持文本、文件和模板三种输入方式。
- **核心功能**:
- **多行文本**: 直接在节点中输入多个提示词。
- **文件支持**: 通过 `.txt` 文件加载提示词。
- **模板功能**: 使用 `{p}` 作为占位符,将模板应用于每一行提示词。
- **运行模式**:
- `all`: 一次性处理所有提示词,生成一个批次。
- `incremental`: 每次执行处理一个提示词。
### 3. Negative Prompt Queue / 负面提示词队列
专门用于管理和应用负面提示词的节点。
- **核心功能**:
- **多行文本**: 输入所有负面提示词。
- **模板预设**: 可以将常用的负面提示词组合保存为预设。
### 4. Simple Prompt Queue / 简化提示词队列
一个精简版的正向提示词队列,仅支持从文件加载,并可以选择预设,但不提供预设的保存和删除功能。
1. **节点拆分**: 将原有的 `Style Prompt Queue` 拆分为两个独立的节点:
* `PQ (Incremental) / 增量队列`: 专注于单次/逐行生成,支持强大的索引控制。
* `PQ (Batch) / 批量队列`: 专注于一次性批量输出。
2. **预设系统优化**:
* **功能增强**: 批量版 (`PQ Batch`) 现已支持完整的预设保存与删除功能。
* **简易版升级**: 简易版 (`PQ Simple`) 现已增加预设选择功能,可直接调用已有预设。
* **文件分离**: 预设数据现已独立存储于 `_prompt_queue_state/presets.json`,方便分享与备份;运行状态隔离在 `state.json`。
* **⚠️ 注意**: 保存或删除预设后,**必须运行一次工作流**以执行操作,然后**刷新浏览器页面**,新的预设列表才会显示在下拉菜单中。
3. **节点重命名**: 采用更紧凑的中英命名(如 `PQ (Incremental) / 增量队列`),显著缩短节点宽度,提升 UI 体验。
4. **移除过时节点**: 移除了旧版的 `PromptQueue` 和 `SimplePromptQueue`,建议全面迁移到新版节点。
5. **全新的索引控制**: 引入了 `索引设置` 参数,替代了旧版的队列标识逻辑,支持更灵活的行选择(重置、锁定、列表循环)。
6. **UI 优化**: 调整了参数顺序,更符合操作直觉:前缀 -> 主体 -> 后缀。
7. **汉化**: 全面汉化了节点参数和选项。
---
## 安装
## 🧩 节点介绍
### 1. PQ (Incremental) / 增量队列 🆕
这是您日常创作的主力节点。它专为**逐行调试**和**稳定生成**设计。
* **核心逻辑**: 每次运行只输出**一行**提示词。
* **容错机制**: 如果您的 KSampler 设置 `Batch Size > 1` (例如 4),该节点会输出 4 份**完全相同**的 conditioning。这意味着您将得到 4 张基于同一提示词的图片,方便优选,提高出图容错率。
* **参数详解**:
* **全局前缀**: 一个全局通用的提示词前缀(例如通用画质词、常用 LoRA 触发词等),**始终拼在最前面**。
* **保存为全局前缀**: 勾选后,会将当前“全局前缀”写入插件全局状态,供后续所有 PQ 节点默认使用。
* **画师风格提示词 (前缀)**: 输入风格、艺术家等前缀。
* **多行文本**: 核心主体提示词列表,每行一个。
* **提示词后缀**: 画面质量、细节描述等通用后缀。
* **使用文件**: 从文件读取提示词列表(每行一个)。
* **提示词文件夹 / 文件名**: 将文件路径拆分为“文件夹 + 文件名”,文件扩展名固定为 `.txt`。
* 示例:文件夹 `C:\Users\Administrator\Desktop\prompt` + 文件名 `21` -> 实际读取 `...\prompt\21.txt`
* **索引设置 (核心功能)**: 控制本次运行使用哪一行提示词。
* `-1` (或留空): **记忆模式**。接着上次的进度继续跑(适合长序列生成)。
* `0`: **顺序模式**。重置计数器,从第 1 行开始,并在每次生成后自动递增(适合想从头完整跑一遍队列)。
* `5` (数字 > 0): **指定模式**。强制只跑第 N 行 (如输入 5 则跑第 5 行),**不会**自动递增(适合反复调试特定提示词)。
* `2,8,35`: **列表模式**。只在第 2、8、35 行之间循环生成(适合精选生成)。
* **文本输出**: 提供 `prompt_text` 输出端口,可连接 ShowText 节点查看当前使用的完整提示词。
### 2. PQ (Batch) / 批量队列 🆕
专为**一次性大批量生成**设计。
* **核心逻辑**: 一次性输出多行提示词的 conditioning。
* **Batch 对应**: 如果 KSampler 设置 `Batch Size = 4`,该节点会分别输出列表中的第 1, 2, 3, 4 行提示词。生成的 4 张图将分别对应这 4 个不同的提示词。
* **预设支持**: 现已支持保存和加载预设模板。
* **⚠️ 注意**: 保存或删除预设后,请**刷新浏览器**以更新预设列表。
* **文本输出**: 提供 `prompt_text_list` 输出端口,输出当前批次的所有提示词文本。
* **提示词来源**: 同样支持“使用文件 + 提示词文件夹/文件名(固定 .txt)”。
### 3. PQ Simple / 简易版系列 🆕
包含 **增量简易** (`PQ Simple (Incr.)`) 和 **批量简易** (`PQ Simple (Batch)`) 两个节点。
* **特点**: 移除了保存和删除预设的复杂参数,界面极简。
* **预设调用**: 保留了 `预设模板` 下拉菜单,可以**读取和使用**在完整版中保存的预设。
* **适用场景**: 当您不需要修改预设,只需要专注于生成时使用。
### 4. Negative PQ / 负面队列
专门用于管理和应用负面提示词的节点。
* **核心功能**:
* **多行文本**: 输入所有负面提示词。
* **预设系统**: 支持保存和加载常用的负面提示词组合。
* **模板支持**: 可以定义模板结构。
---
## 🛠️ 安装
1. 打开命令行工具。
2. 进入 ComfyUI 的 `custom_nodes` 目录:
@@ -60,42 +90,32 @@
---
## 快速上手
## 🚀 快速上手 (PQ Incremental)
### 使用 `Style Prompt Queue`
1. **添加节点**: 在 ComfyUI 中添加 `Style Prompt Queue` 节点。
1. **添加节点**: 在 ComfyUI 中添加 `PQ (Incremental) / 增量队列` 节点。
2. **连接 CLIP**: 将上游的 `CLIP` 输出连接到此节点的 `clip` 输入。
3. **填写提示词**:
- 在 **`artist_style_prompt`** 中填入风格,例如 `by Makoto Shinkai, cinematic lighting`。
- 在 **`multiline_text`** 中填入主体,每行一个,例如:
```
a girl on the beach
a boy in the park
a robot on a tree
```
- 在 **`prompt_suffix`** 中填入后缀,例如 `best quality, masterpiece, 8k`。
4. **连接输出**: 将此节点的 `positive` 输出连接到采样器(KSampler)的 `positive` 输入。
5. **运行**: 点击 `Queue Prompt`。每次执行,它会自动处理 `multiline_text` 中的下一行,并与前后缀组合成最终的提示词。
### 保存和使用预设 (`Style Prompt Queue`)
1. **填写内容**: 在 `artist_style_prompt` 和 `prompt_suffix` 中填入你想要保存的组合。
2. **命名预设**: 在 `preset_name_for_save` 中为你的预设命名,例如 `电影感-通用`。
3. **保存**: 勾选 `save_preset` 并运行一次工作流。预设即被保存。
4. **使用预设**: 从 `preset` 下拉菜单中选择你刚才保存的预设,节点会自动填入对应的前后缀。
3. **配置提示词**:
* **画师风格**: `masterpiece, best quality, by Makoto Shinkai`
* **多行文本**:
```
a girl on the beach
a boy in the park
a robot on a tree
```
* **后缀**: `8k, cinematic lighting`
4. **调试**: 将 `索引设置` 设为 `0`,确保每次点击生成都从第一行 "a girl..." 开始。
5. **批量容错**: 将 KSampler 的 `batch_size` 设为 4。点击生成,你将获得 4 张不同随机种子的 "a girl on the beach"。
6. **自动化**: 将 `索引设置` 改回 `-1`,再次点击生成,它将自动跳到 "a boy..."。
---
## 常见问题 (FAQ)
## ❓ 常见问题 (FAQ)
- **`队列标识 (label)` 的作用是什么?**
- 它为每个队列实例提供一个独立的ID。如果你在同一个工作流中使用了多个队列节点,请确保它们的 `label` 是不同的,这样它们的执行进度才不会互相干扰。
* **为什么删除了旧的 PromptQueue?**
* 旧节点功能较为基础,且“运行模式”参数容易混淆。新的 Style 系列节点通过分离“增量”和“批量”逻辑,并引入强大的“索引设置”,完全覆盖并增强了旧节点的功能。
* **预设保存在哪里?**
* 预设数据现在独立存储在 `ComfyUI/custom_nodes/ComfyUI_PromptQueue/_prompt_queue_state/presets.json` 文件中。您可以直接复制该文件来备份或分享您的预设。
* (旧版的 `state.json` 现仅用于存储运行计数器)。
- **`incremental` 和 `all` 模式有什么区别?**
- `all`: 一次性处理所有提示词,将它们打包成一个批次输出。适合需要一次性生成所有结果的场景。
- `incremental`: 每次执行只处理队列中的下一个提示词。适合自动化、长序列的生成,例如动画帧。
- **预设保存在哪里?**
- 所有的预设数据都保存在 `ComfyUI/custom_nodes/ComfyUI_PromptQueue/_prompt_queue_state/state.json` 文件中。
* **保存或删除预设后,为什么下拉菜单没变化?**
* ComfyUI 的节点菜单是在页面加载时初始化的。保存/删除操作是在后端执行的,因此您需要**运行一次工作流**(让后端执行保存动作),然后**刷新浏览器页面**(重新加载前端菜单),才能看到更新后的预设列表。
+20
View File
@@ -0,0 +1,20 @@
{
"Template Presets Negative": {
"PonyNeg,": "{p},score_6,score_5,score_4,simplified,abstract,unrealistic,impressionistic,low resolution,lowres,bad anatomy,bad hands,missing fingers,worst quality,low quality,normal quality,cartoon,anime,drawing,sketch,illustration,artificial,poor quality,nsfw,worst quality,old,early,low quality,lowres,signature,username,logo,bad hands,mutated hands,mammal,anthro,furry,ambiguous form,feral,semi-anthro,",
"NovaNeg,": "{p},modern, recent, old, oldest, cartoon, graphic, text, painting, crayon, graphite, abstract, glitch, deformed, mutated, ugly, disfigured, long body, lowres, bad anatomy, bad hands, missing fingers, extra digits, fewer digits, cropped, very displeasing, (worst quality, bad quality:1.2), sketch, jpeg artifacts, signature, watermark, username, simple background, conjoined, bad ai-generated",
"NOVAFurryNeg,": "{p},human, multiple tails, modern, recent, old, oldest, graphic, cartoon, text, painting, crayon, graphite, abstract, glitch, deformed, mutated, ugly, disfigured, long body, lowres, bad anatomy, bad hands, missing fingers, extra digits, fewer digits, cropped, very displeasing, (worst quality, bad quality:1.2), bad anatomy, sketch, jpeg artifacts, signature, watermark, username, simple background, conjoined, bad ai-generated",
"NoobAINeg,": "{p},worst quality, old, early, low quality, lowres, signature, username, logo, bad hands, mutated hands, mammal, anthro, furry, ambiguous form, feral, semi-anthro,"
},
"Template Presets Positive": {
"Pony": "score_9, score_8_up, score_7_up, (SUBJECT),{p}",
"Pony_realistic": "score_9, score_8_up, score_7_up, source_real, real_skin,{p}",
"Nova": "masterpiece, best quality, amazing quality, 4k, very aesthetic, high resolution, ultra-detailed, absurdres, newest, scenery, {P}, BREAK, depth of field, volumetric lighting",
"NovaFurry": "masterpiece, best quality, amazing quality, very aesthetic, high resolution, ultra-detailed, absurdres, newest, scenery, furry, anthro, {P}, BREAK, depth of field, detailed fluffy fur, volumetric lighting,",
"NoobAI": "{p},masterpiece, best quality, newest, absurdres, highres, amazing quality,",
"Nai4-hot": "(misaka 12003-gou:1.2),(dino\\(dinoartforame):0.8),(wanke:0.8),(liduke:0.8),realistic,4k,photorealistic,bokeh,chromatic_aberration,depth of field,{p},masterpiece,best quality,newest,absurdres,highres,amazing quality,Cinematic,",
"Youthful,sensual,semi-realistic": "{artist:omone_hokoma_agm,[artist:wlop],[artist:nababa]},[[kidmo]],,{p},masterpiece,best quality,newest,absurdres,highres,amazing quality,Cinematic,",
"Mature,sensual,semi-realistic": "[[[kedama_milk]]],[sanuki_(kyoudashya)],murata_yuusuke,[[kidmo]],,{p},masterpiece,best quality,newest,absurdres,highres,amazing quality,Cinematic,",
"nsfw-masterpiece": "nsfw,{p},masterpiece,best quality,newest,absurdres,highres,amazing quality,",
"Realistic": "{p},photorealistic, ultra-detailed, best quality, high detail, HDR, film grain, masterpiece,"
}
}
+4
View File
@@ -0,0 +1,4 @@
{
"PromptQueue Sources": {},
"PromptQueue Counters": {}
}
+445 -215
View File
@@ -20,18 +20,61 @@ NODE_DISPLAY_NAME_MAPPINGS = {}
PARAMETER_LABELS = {
"multiline_text": "多行文本",
"use_file": "使用文件",
"file_path": "文件路径",
"file_folder": "提示词文件夹",
"file_name": "文件名",
"mode": "运行模式",
"label": "队列标识",
"preset": "预设模板",
"template": "模板",
"save_preset": "保存预设",
"delete_preset": "删除预设",
"global_prefix": "全局前缀",
"save_global_prefix": "保存为全局前缀",
"artist_style_prompt": "画师风格提示词",
"prompt_suffix": "提示词后缀",
"preset_name_for_save": "保存预设名称"
"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
_PQ_GLOBAL_PREFIX_CATEGORY = "Global"
_PQ_GLOBAL_PREFIX_KEY = "Global Prefix"
def _pq_get_global_prefix() -> str:
return str(_pq_store.get(_PQ_GLOBAL_PREFIX_CATEGORY, _PQ_GLOBAL_PREFIX_KEY, "") or "")
def _pq_set_global_prefix(value: str) -> None:
_pq_store.put(_PQ_GLOBAL_PREFIX_CATEGORY, _PQ_GLOBAL_PREFIX_KEY, str(value or ""))
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} 的升序字典。
@@ -142,9 +185,6 @@ def encode_prompts_sequential(clip, ordered_items):
return cond_final, pooled_final, pooled_available
## 删除了 BatchPrompt 节点,保留 PromptQueue
def _read_lines_from_source(multiline_text: str, file_path: str = "", use_file: bool = False):
"""
从多行文本或文件读取,每行一个提示词;自动去除空行和首尾空白。
@@ -178,90 +218,36 @@ def _apply_template_to_lines(lines, template: str):
return [template.replace("{p}", line) for line in lines]
class SimplePromptQueue:
def _parse_index_setting(index_str: str) -> list:
"""
简化版提示词队列节点,支持从文件读取和预设模板功能。
将多行提示词视为队列,从 txt 文件读取(逐行)。
输出按批次堆叠的 CONDITIONING,便于一次性批量生成。
解析索引设置字符串。
支持格式:
- "-1" 或 "": 记忆模式 (Auto/Resume) -> 返回 [-1]
- "0": 重置模式 (Reset to 0) -> 返回 [0]
- "5": 单行模式 -> 返回 [5] (第6行)
- "2,8,35": 列表模式 -> 返回 [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": ""}),
"运行模式": (["all", "incremental"], {"default": "all"}),
"队列标识": ("STRING", {"multiline": False, "default": "Simple Queue 001"}),
"预设模板": (preset_list, {"default": "None"}),
}
}
RETURN_TYPES = ("CONDITIONING",)
RETURN_NAMES = ("positive",)
FUNCTION = "run"
CATEGORY = "PromptQueue"
def run(self, clip, 文件路径, 运行模式="all", 队列标识="Simple Queue 001", 预设模板="None"):
# 选择预设(若为 None 则不应用模板)
模板 = ""
if 预设模板 != "None":
presets = _pq_load_positive_template_presets()
if 预设模板 in presets:
模板 = presets[预设模板]
print(f"[Simple Prompt Queue] 使用预设 '{预设模板}': {模板}")
else:
print(f"[Simple Prompt Queue] 预设 '{预设模板}' 不存在,忽略模板")
if not 文件路径.strip():
dummy = torch.zeros(1, 1)
# 创建正确维度的 dummy pooled tensor (SDXL 需要 2816 维)
dummy_pooled = torch.zeros(1, 2816)
return ([[dummy, {"pooled_output": dummy_pooled}]],)
# 从文件读取提示词
lines = _read_lines_from_source("", 文件路径, True)
lines = _apply_template_to_lines(lines, 模板)
if not lines:
dummy = torch.zeros(1, 1)
# 创建正确维度的 dummy pooled tensor (SDXL 需要 2816 维)
dummy_pooled = torch.zeros(1, 2816)
return ([[dummy, {"pooled_output": dummy_pooled}]],)
# 模式:incremental 每次调用消费一行,使用每个标签的持久计数器
if 运行模式 == "incremental":
idx = _pq_get_next_index(队列标识, _pq_total=len(lines), source_descriptor=_pq_source_descriptor(True, 文件路径, ""))
line = lines[idx]
ordered = [(0, line)]
cond, pooled, has_pooled = encode_prompts_sequential(clip, ordered)
meta = {"pooled_output": pooled}
return ([[cond, meta]],)
# 默认:所有行一起批处理
ordered = list(enumerate(lines))
cond, pooled, has_pooled = encode_prompts_sequential(clip, ordered)
meta = {"pooled_output": pooled}
return ([[cond, meta]],)
@classmethod
def IS_CHANGED(cls, clip, 文件路径, 运行模式="all", 队列标识="Simple Queue 001", 预设模板="None"):
# 确保增量模式每次重新执行以推进队列
if 运行模式 == "incremental":
return str(time.time())
# 当预设更改时强制重新执行以实时更新模板
if 预设模板 != "None":
return str(time.time())
# 对于确定性模式,对输入进行哈希以便缓存按预期工作
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:
desc = _pq_source_descriptor(True, str(文件路径), "")
key = f"{desc}|{运行模式}|{队列标识}|{预设模板}"
return hashlib.sha1(key.encode("utf-8", errors="ignore")).hexdigest()
except Exception:
return ""
val = int(p)
indices.append(val)
except ValueError:
continue
if not indices:
return [-1]
return indices
class NegativePromptQueue:
@@ -379,105 +365,12 @@ class NegativePromptQueue:
return ""
class PromptQueue:
"""
将多行提示词视为队列,或从 txt 文件读取(逐行)。
输出按批次堆叠的 CONDITIONING,便于一次性批量生成。
可选模板,将每行填充到模板中的 {prompt} 占位符。
"""
@classmethod
def INPUT_TYPES(cls):
# 动态获取预设列表(正面)
presets = _pq_load_positive_template_presets()
preset_list = ["None"] + list(presets.keys())
return {
"required": {
"clip": ("CLIP",),
"多行文本": ("STRING", {"multiline": True, "default": "A cat\nA dog\nA bird"}),
"使用文件": ("BOOLEAN", {"default": False}),
"文件路径": ("STRING", {"multiline": False, "default": ""}),
"运行模式": (["all", "incremental"], {"default": "all"}),
"队列标识": ("STRING", {"multiline": False, "default": "Prompt Queue 001"}),
"预设模板": (preset_list, {"default": "None"}),
"模板": ("STRING", {"multiline": False, "default": "{p}"}),
"保存预设": ("BOOLEAN", {"default": False}),
"删除预设": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = ("CONDITIONING",)
RETURN_NAMES = ("positive",)
FUNCTION = "run"
CATEGORY = "PromptQueue"
def run(self, clip, 多行文本, 使用文件, 文件路径, 模板, 运行模式="all", 队列标识="Prompt Queue 001", 预设模板="None", 保存预设=False, 删除预设=False):
# 处理删除预设 - 拥有最高优先级
if 删除预设 and 预设模板 != "None":
_pq_delete_positive_template_preset(预设模板)
print(f"[Prompt Queue] 已删除预设 '{预设模板}'")
print(f"[Prompt Queue] 使用 template 内容(删除预设后): {模板}")
# 处理保存预设 - 第二优先级,强制使用 template 内容
elif 保存预设 and 模板.strip():
_pq_save_positive_template_preset(模板.strip(), 模板.strip())
print(f"[Prompt Queue] 已保存预设 '{模板.strip()}': {模板.strip()}")
print(f"[Prompt Queue] 使用 template 内容(save_preset 优先级): {模板}")
# 处理预设选择 - 当选择非"None"预设且未开启保存/删除时,使用预设内容
elif 预设模板 != "None":
presets = _pq_load_positive_template_presets()
if 预设模板 in presets:
模板 = presets[预设模板]
print(f"[Prompt Queue] 使用预设 '{预设模板}': {模板}")
else:
print(f"[Prompt Queue] 预设 '{预设模板}' 不存在,使用 template 内容")
else:
print(f"[Prompt Queue] 使用 template 内容: {模板}")
lines = _read_lines_from_source(多行文本, 文件路径, 使用文件)
lines = _apply_template_to_lines(lines, 模板)
if not lines:
dummy = torch.zeros(1, 1)
# 创建正确维度的 dummy pooled tensor (SDXL 需要 2816 维)
dummy_pooled = torch.zeros(1, 2816)
return ([[dummy, {"pooled_output": dummy_pooled}]],)
# 模式:incremental 每次调用消费一行,使用每个标签的持久计数器
if 运行模式 == "incremental":
idx = _pq_get_next_index(队列标识, _pq_total=len(lines), source_descriptor=_pq_source_descriptor(使用文件, 文件路径, 多行文本))
line = lines[idx]
ordered = [(0, line)]
cond, pooled, has_pooled = encode_prompts_sequential(clip, ordered)
meta = {"pooled_output": pooled}
return ([[cond, meta]],)
# 默认:所有行一起批处理
ordered = list(enumerate(lines))
cond, pooled, has_pooled = encode_prompts_sequential(clip, ordered)
meta = {"pooled_output": pooled}
return ([[cond, meta]],)
@classmethod
def IS_CHANGED(cls, clip, 多行文本, 使用文件, 文件路径, 模板, 运行模式="all", 队列标识="Prompt Queue 001", 预设模板="None", 保存预设=False, 删除预设=False):
# 确保增量模式每次重新执行以推进队列
if 运行模式 == "incremental":
return str(time.time())
# 当预设更改时强制重新执行以实时更新模板
if 预设模板 != "None":
return str(time.time())
# 对于确定性模式,对输入进行哈希以便缓存按预期工作
try:
desc = _pq_source_descriptor(bool(使用文件), str(文件路径), 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):
@@ -488,12 +381,15 @@ class StylePromptQueue:
return {
"required": {
"clip": ("CLIP",),
"全局前缀": ("STRING", {"multiline": False, "default": ""}),
"保存为全局前缀": ("BOOLEAN", {"default": False}),
"画师风格提示词": ("STRING", {"multiline": False, "default": ""}),
"多行文本": ("STRING", {"multiline": True, "default": "A cat\nA dog"}),
"使用文件": ("BOOLEAN", {"default": False}),
"文件路径": ("STRING", {"multiline": False, "default": ""}),
"提示词后缀": ("STRING", {"multiline": False, "default": ""}),
"队列标识": ("STRING", {"multiline": False, "default": "Style Queue 001"}),
"使用文件": ("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}),
@@ -501,12 +397,17 @@ class StylePromptQueue:
}
}
RETURN_TYPES = ("CONDITIONING",)
RETURN_NAMES = ("positive",)
RETURN_TYPES = ("CONDITIONING", "STRING")
RETURN_NAMES = ("positive", "prompt_text")
FUNCTION = "run"
CATEGORY = "PromptQueue"
def run(self, clip, 画师风格提示词, 多行文本, 使用文件, 文件路径, 提示词后缀, 队列标识, 预设模板, 保存预设名称, 保存预设, 删除预设):
def run(self, clip, 全局前缀, 保存为全局前缀, 画师风格提示词, 多行文本, 提示词后缀, 使用文件, 提示词文件夹, 文件名, 索引设置, 预设模板, 保存预设名称, 保存预设, 删除预设, unique_id=None):
# 处理全局前缀(独立于预设逻辑,永远保留最高层级)
if 保存为全局前缀:
_pq_set_global_prefix(全局前缀)
global_prefix = _pq_normalize_prompt_part(全局前缀) or _pq_normalize_prompt_part(_pq_get_global_prefix())
prefix = 画师风格提示词
suffix = 提示词后缀
@@ -560,36 +461,338 @@ class StylePromptQueue:
prefix = preset_content
suffix = ""
print(f"[Style Prompt Queue] 无法解析预设 '{预设模板}',已作为前缀处理")
lines = _read_lines_from_source(多行文本, 文件路径, 使用文件)
# 组合文件路径(优先“文件夹+文件名”,回退到“文件路径”以兼容旧工作流)
effective_file_path = _pq_build_prompt_file_path(提示词文件夹, 文件名)
lines = _read_lines_from_source(多行文本, effective_file_path, 使用文件)
# 应用前缀和后缀
final_lines = []
for line in lines:
parts = [p for p in [prefix, line, suffix] if p and p.strip()]
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:
# 返回空的 conditioning
dummy = torch.zeros(1, 1)
dummy_pooled = torch.zeros(1, 2816)
return ([[dummy, {"pooled_output": dummy_pooled}]],)
return ([[dummy, {"pooled_output": dummy_pooled}]], "")
# 始终为增量模式
idx = _pq_get_next_index(队列标识, _pq_total=len(final_lines), source_descriptor=_pq_source_descriptor(使用文件, 文件路径, 多行文本))
line_to_process = final_lines[idx]
# --- 核心索引控制逻辑 ---
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]],)
return ([[cond, meta]], line_to_process)
@classmethod
def IS_CHANGED(cls, *args, **kwargs):
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": ""}),
"保存为全局前缀": ("BOOLEAN", {"default": False}),
"画师风格提示词": ("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": ""}),
"保存为全局前缀": ("BOOLEAN", {"default": False}),
"画师风格提示词": ("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, 全局前缀, 保存为全局前缀, 画师风格提示词, 多行文本, 提示词后缀, 使用文件, 提示词文件夹, 文件名, 索引设置, 预设模板, 保存预设名称, 保存预设, 删除预设):
if 保存为全局前缀:
_pq_set_global_prefix(全局前缀)
global_prefix = _pq_normalize_prompt_part(全局前缀) or _pq_normalize_prompt_part(_pq_get_global_prefix())
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": ""}),
"保存为全局前缀": ("BOOLEAN", {"default": False}),
"画师风格提示词": ("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):
@@ -623,10 +826,39 @@ class _JsonKVStore:
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:
@@ -657,49 +889,47 @@ def _pq_get_next_index(label: str, _pq_total: int, source_descriptor: str) -> in
def _pq_save_positive_template_preset(name: str, template: str) -> None:
"""保存正面模板预设"""
_pq_store.put("Template Presets Positive", name, template)
_preset_store.put("Template Presets Positive", name, template)
def _pq_load_positive_template_presets() -> dict:
"""加载所有正面模板预设"""
return _pq_store.data.get("Template Presets Positive", {})
return _preset_store.data.get("Template Presets Positive", {})
def _pq_delete_positive_template_preset(name: str) -> None:
"""删除正面模板预设"""
if "Template Presets Positive" in _pq_store.data and name in _pq_store.data["Template Presets Positive"]:
del _pq_store.data["Template Presets Positive"][name]
_pq_store._save()
_preset_store.remove("Template Presets Positive", name)
def _pq_save_negative_template_preset(name: str, template: str) -> None:
"""保存负面模板预设"""
_pq_store.put("Template Presets Negative", name, template)
_preset_store.put("Template Presets Negative", name, template)
def _pq_load_negative_template_presets() -> dict:
"""加载所有负面模板预设"""
return _pq_store.data.get("Template Presets Negative", {})
return _preset_store.data.get("Template Presets Negative", {})
def _pq_delete_negative_template_preset(name: str) -> None:
"""删除负面模板预设"""
if "Template Presets Negative" in _pq_store.data and name in _pq_store.data["Template Presets Negative"]:
del _pq_store.data["Template Presets Negative"][name]
_pq_store._save()
_preset_store.remove("Template Presets Negative", name)
# 注册新节点
NODE_CLASS_MAPPINGS.update({
"PromptQueue": PromptQueue,
"SimplePromptQueue": SimplePromptQueue,
"NegativePromptQueue": NegativePromptQueue,
"StylePromptQueue": StylePromptQueue,
"SimpleStylePromptQueue": SimpleStylePromptQueue,
"StylePromptQueueBatch": StylePromptQueueBatch,
"SimpleStylePromptQueueBatch": SimpleStylePromptQueueBatch,
})
NODE_DISPLAY_NAME_MAPPINGS.update({
"PromptQueue": "Prompt Queue / 提示词队列",
"SimplePromptQueue": "Simple Prompt Queue / 简化提示词队列",
"NegativePromptQueue": "Negative Prompt Queue / 负面提示词队列",
"StylePromptQueue": "Style Prompt Queue / 风格化提示词队列",
"NegativePromptQueue": "Negative PQ / 负面队列",
"StylePromptQueue": "PQ (Incremental) / 增量队列",
"SimpleStylePromptQueue": "PQ Simple (Incr.) / 增量简易",
"StylePromptQueueBatch": "PQ (Batch) / 批量队列",
"SimpleStylePromptQueueBatch": "PQ Simple (Batch) / 批量简易",
})