feat: add prompt tools and Anima workflow scheme

- Add Mustache vars/render + Text Input helpers
- Add JSON Parse + Prompt JSON Unpack (typed outputs + vars)
- Add Markdown Preview (marked + DOMPurify)
- Add Anima prompt/scheme docs + example workflow
- Add GitHub Wiki auto-publish workflow (docs/wiki -> wiki)
This commit is contained in:
Nakano Kenji
2026-02-01 22:50:16 +08:00
parent 34c190fffc
commit 87aff43e63
28 changed files with 1704 additions and 1 deletions
+71
View File
@@ -0,0 +1,71 @@
name: Publish Wiki
on:
push:
branches:
- main
- master
paths:
- "docs/wiki/**"
- ".github/workflows/publish-wiki.yml"
workflow_dispatch: {}
permissions:
contents: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Sync docs/wiki -> GitHub Wiki
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
if [ ! -d "docs/wiki" ]; then
echo "::error::docs/wiki does not exist"
exit 1
fi
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
wiki_repo="https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.wiki.git"
rm -rf wiki
if ! git ls-remote "$wiki_repo" > /dev/null 2>&1; then
echo "::error::Unable to access wiki repo. Ensure Wiki is enabled in repository Settings -> Features -> Wiki."
exit 1
fi
mkdir -p wiki
cd wiki
git init
git remote add origin "$wiki_repo"
git fetch origin || true
if git show-ref --quiet refs/remotes/origin/master; then
git checkout -B master origin/master
elif git show-ref --quiet refs/remotes/origin/main; then
git checkout -B main origin/main
else
git checkout -B master
fi
cd ..
rsync -av --delete --exclude ".git" docs/wiki/ wiki/
cd wiki
git add -A
if git diff --cached --quiet; then
echo "No wiki changes to publish."
exit 0
fi
git commit -m "Sync wiki from docs/wiki (${GITHUB_SHA})"
git push origin HEAD
+3 -1
View File
@@ -16,5 +16,7 @@ comfyworkflows_sharekey
github-stats-cache.json
pip_overrides.json
*.json
# Allow version-controlled workflow examples / assets
!assets/*.json
check2.sh
/venv/
/venv/
+47
View File
@@ -16,6 +16,15 @@ Simple, no-nonsense LLM chat nodes for ComfyUI. Connect to OpenAI, Claude, Gemin
- **Multimodal Support**: Chat with images using vision-capable models.
- **NoASS Roleplay Mode (Exp)**: Experimental Hardcore roleplay node with Assistant Prefill support.
- **Gemini Native Features**: Dedicated nodes for Gemini's image generation and editing capabilities.
- **Template Variables (Mustache)**: Write `{{var}}` in any prompt/system field and inject values via a node.
- **JSON Field Extractor**: Parse JSON and split values into multiple outputs (fixed 8 ports).
- **Markdown Preview**: Render markdown output in a safe, sanitized popup.
## Wiki (GitHub Native)
- **Wiki Home**: `https://github.com/Moeblack/ComfyUI-SimpleChat/wiki`
- **Source of truth**: `docs/wiki/` (auto-published by GitHub Actions)
- **Note**: Ensure Wiki is enabled in your repo Settings -> Features -> Wiki
## Installation
@@ -56,6 +65,7 @@ Use this for standard text generation or Q&A.
* **Connection**: Connect `config` from **API Config** to `config` on **Chat**.
* **Input**: Enter your prompt in `text`.
* **Output**: Returns the AI's response as a string.
* **Template vars (optional)**: Connect `Mustache Var` output `vars` to `Chat` input `vars`, then you can use `{{var}}` in `prompt/system` and it will be replaced.
### 3. Chat with Images
Use this to have the AI analyze or describe an image.
@@ -89,6 +99,43 @@ Modify existing images using text instructions.
* **Node**: `Gemini Image Edit`
* **Input**: Source image + Mask image + Prompt.
### 7. JSON Parse
Parse an LLM-produced JSON string and extract up to 8 fields.
* **Node**: `JSON Parse`
* **Inputs**:
* `json_text`: JSON string (can auto-extract from ```json ... ``` fences)
* `path1..path8`: dot/bracket paths like `a.b.c` or `items[0].name`
* **Outputs**: `out1..out8` + `obj` (the full parsed object)
### 7.1 Prompt JSON Unpack (recommended)
If your LLM outputs a fixed JSON schema (positive/negative/width/height/steps/cfg/sampler/seed/notes), use this node to unpack everything with **proper typed outputs** (INT/FLOAT/SAMPLER) so you can wire directly into ComfyUI nodes without manual paths or casting.
* **Node**: `Prompt JSON Unpack`
* **Input**: `json_text` (can auto-extract from ```json ... ``` fences)
* **Outputs**:
* `positive` / `negative` (STRING)
* `width` / `height` / `steps` / `seed` (INT)
* `cfg` (FLOAT)
* `sampler` (SAMPLER)
* `notes` (STRING)
* `vars` (SIMPLECHAT_VARS mapping for Mustache: `{{positive}}`, `{{width}}`, etc.)
### 8. Markdown Preview
Preview markdown content in a popup (sanitized).
* **Node**: `Markdown Preview`
* **Usage**: Connect any text output to `text`, execute to open the preview popup.
### 9. Anima prompt template (docs)
If you use SimpleChat to have an LLM generate Anima prompts, see:
* `docs/anima_prompt.md`
* `docs/anima_scheme.md` (JSON fields + built-in Mustache vars scheme)
(Optional) Example workflow:
* `assets/AnimaPromptScheme.json`
## FAQ
**Q: I don't see my model in the list.**
+47
View File
@@ -16,6 +16,15 @@ ComfyUI 的极简 LLM 聊天节点。轻松连接 OpenAI, Claude, Gemini 以及
- **多模态支持**: 使用具备视觉能力的模型与图片进行对话。
- **NoASS 角色扮演模式 (实验性)**: 专为硬核角色扮演设计的节点,支持 Assistant Prefill (预填) 技术。
- **Gemini 原生功能**: 专为 Gemini 设计的文生图和图片编辑节点。
- **模板变量 (Mustache)**: 在 prompt/system 等任意文本里写 `{{变量名}}`,通过节点统一注入替换。
- **JSON 解析拆字段**: 将 JSON 自动解析并按 path 输出到多个端口(固定 8 路)。
- **Markdown 渲染预览**: 将文本以 Markdown 方式弹窗渲染展示(已做净化,避免脚本注入)。
## Wiki(GitHub 原生)
- **Wiki 首页**:`https://github.com/Moeblack/ComfyUI-SimpleChat/wiki`
- **仓库内维护源**:`docs/wiki/`(由 GitHub Actions 自动同步发布)
- **注意**:需要在 GitHub 仓库 Settings -> Features -> 勾选 Wiki
## 安装 (Installation)
@@ -56,6 +65,7 @@ git clone https://github.com/Moeblack/ComfyUI-SimpleChat.git
* **连接**: 将 **API Config** 的 `config` 输出连接到 **Chat** 的 `config` 输入。
* **输入**: 在 `text` 中输入您的提示词。
* **输出**: 返回 AI 的回复字符串。
* **模板变量(可选)**: 将 `Mustache Var` 的 `vars` 输出接到 `Chat` 的 `vars` 输入后,`prompt/system` 中支持写 `{{变量名}}` 并自动替换。
### 3. 图片对话 (Chat with Images)
让 AI 分析或描述图片。
@@ -89,6 +99,43 @@ git clone https://github.com/Moeblack/ComfyUI-SimpleChat.git
* **节点**: `Gemini Image Edit`
* **输入**: 原图 (Source image) + 遮罩 (Mask image) + 提示词 (Prompt)。
### 7. JSON 解析拆字段 (JSON Parse)
把 LLM 输出的 JSON 自动解析成多个字段,便于接入后续节点。
* **节点**: `JSON Parse`
* **输入**:
* `json_text`: JSON 字符串(支持从 ```json ... ``` 代码块中自动提取)
* `path1..path8`: 取值路径(支持 `a.b.c` 与 `items[0].name`)
* **输出**: `out1..out8` + `obj`(完整解析后的对象)
### 7.1 Prompt JSON 拆包(强烈推荐)
如果你的 LLM 输出遵循固定 JSON 格式(positive/negative/width/height/steps/cfg/sampler/seed/notes),推荐用这个节点“一键拆包”,并且 **width/height/steps/cfg/seed/sampler 都是带类型输出**,可直接接到 ComfyUI 的对应输入口(不用手动填 path,也不用字符串转数字)。
* **节点**: `Prompt JSON Unpack`
* **输入**: `json_text`(支持从 ```json ... ``` 代码块中自动提取)
* **输出**:
* `positive` / `negative`(STRING)
* `width` / `height` / `steps` / `seed`(INT)
* `cfg`(FLOAT)
* `sampler`(SAMPLER)
* `notes`(STRING)
* `vars`(SIMPLECHAT_VARS:自动把这些字段映射成 `{{positive}}`/`{{width}}` 等胡子变量,另含中文别名)
### 8. Markdown 渲染预览 (Markdown Preview)
把文本以 Markdown 渲染的方式展示(弹窗),适合预览 LLM 的结构化输出/说明文档。
* **节点**: `Markdown Preview`
* **用法**: 把任意文本输出接到 `text`,执行后自动弹窗渲染。
### 9. Anima 提示词模板(文档)
如果你用 SimpleChat 让 LLM 帮你写 Anima 的提示词,推荐直接用这份 System Prompt:
* `docs/anima_prompt.md`
* `docs/anima_scheme.md`(JSON 字段 + 内置胡子变量命名方案)
(可选)示例工作流:
* `assets/AnimaPromptScheme.json`
## 常见问题 (FAQ)
**Q: 列表里找不到我要的模型。**
+18
View File
@@ -11,6 +11,12 @@ from .nodes import (
SimpleChatNoASS,
GeminiImageGen,
GeminiImageEdit,
SimpleChatMustacheVar,
SimpleChatMustacheRender,
SimpleChatTextInput,
SimpleChatJsonParse,
SimpleChatMarkdownPreview,
SimpleChatPromptJsonUnpack,
)
# Register API routes
@@ -25,6 +31,12 @@ NODE_CLASS_MAPPINGS = {
"SimpleChatNoASS": SimpleChatNoASS,
"GeminiImageGen": GeminiImageGen,
"GeminiImageEdit": GeminiImageEdit,
"SimpleChatMustacheVar": SimpleChatMustacheVar,
"SimpleChatMustacheRender": SimpleChatMustacheRender,
"SimpleChatTextInput": SimpleChatTextInput,
"SimpleChatJsonParse": SimpleChatJsonParse,
"SimpleChatMarkdownPreview": SimpleChatMarkdownPreview,
"SimpleChatPromptJsonUnpack": SimpleChatPromptJsonUnpack,
}
# Display names for the UI
@@ -35,6 +47,12 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"SimpleChatNoASS": "Chat NoASS",
"GeminiImageGen": "Gemini Image Gen",
"GeminiImageEdit": "Gemini Image Edit",
"SimpleChatMustacheVar": "Mustache Var",
"SimpleChatMustacheRender": "Mustache Render",
"SimpleChatTextInput": "Text Input",
"SimpleChatJsonParse": "JSON Parse",
"SimpleChatMarkdownPreview": "Markdown Preview",
"SimpleChatPromptJsonUnpack": "Prompt JSON Unpack",
}
# Expose web directory for JS extensions
+319
View File
@@ -0,0 +1,319 @@
{
"id": "a6f6a4f0-9e1a-4c34-bcf9-8b2d9bb78c2a",
"revision": 0,
"last_node_id": 6,
"last_link_id": 5,
"nodes": [
{
"id": 1,
"type": "SimpleChatConfig",
"pos": [
-1248.0,
360.0
],
"size": [
270,
178
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "config",
"type": "SIMPLECHAT_CONFIG",
"links": [
1
]
}
],
"properties": {
"aux_id": "Moeblack/ComfyUI-SimpleChat",
"Node name for S&R": "SimpleChatConfig"
},
"widgets_values": [
"openai",
"",
"",
"refresh",
""
]
},
{
"id": 2,
"type": "SimpleChatText",
"pos": [
-820.0,
360.0
],
"size": [
430,
240
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "config",
"type": "SIMPLECHAT_CONFIG",
"link": 1
}
],
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
2
]
}
],
"properties": {
"aux_id": "Moeblack/ComfyUI-SimpleChat",
"Node name for S&R": "SimpleChatText"
},
"widgets_values": [
"在这里写你的绘画需求(建议包含:人数/角色外观/构图/光照/背景/画风/安全标签/画师@):\n\n示例:\n1girl, black long hair, red eyes, black trench coat, holding umbrella, rainy night neon street, cel shading + slight painterly, half body, cold mood. safe. artist ref: @xxx",
"你是“Anima(circlestone-labs/Anima)专用提示词工程师”,目标是在 ComfyUI 里用 Anima 生成高质量二次元/插画向图像(非写实、非摄影)。\n\n### 模型规则(必须遵守)\n1) 你必须输出“可直接粘贴到 ComfyUI”的提示词与参数建议(通过 JSON 字段给出)。\n2) Anima 支持 Danbooru 风格 tags + 自然语言混合输入;输出时以 tags 为主,自然语言必须至少 2 句(多人/复杂构图时更要写清楚)。\n3) Anima 标签顺序固定为:\n [质量/元数据/年份/安全标签] [人物数量] [角色名] [作品名] [画师] [一般描述标签]\n4) 画师标签必须以 @ 开头(例如 @xxx),否则影响很弱。\n5) 默认生成参数(可按用户需求微调):\n - 分辨率:约 1MP(如 1024x1024 / 896x1152 / 1152x896)\n - Steps:30–50\n - CFG:4–5\n - Sampler:优先 er_sde;也可 euler_a;想更“发散/创意”可 dpmpp_2m_sde_gpu\n6) 安全标签必须明确:safe / sensitive / nsfw / explicit;并在 negative 里加入相反约束(例如 positive 里是 safe,则 negative 要包含 nsfw, explicit)。\n7) 不要追求写实:避免 photo, realistic, 真实摄影、皮肤毛孔等措辞;应强调 illustration, anime style, painterly, lineart 等。\n8) 若用户想要非二次元风格:positive 第一行写 dataset tag(ye-pop 或 deviantart),换行后再给标题/描述,再给正常标签行。\n\n### 输出格式(必须是 JSON,且不要夹带额外解释文字)\n输出一个 JSON 对象,键固定如下:\n{\n \"positive\": \"...\",\n \"negative\": \"...\",\n \"width\": 1024,\n \"height\": 1024,\n \"steps\": 40,\n \"cfg\": 4.5,\n \"sampler\": \"er_sde\",\n \"seed\": -1,\n \"notes\": \"1-3条简短技巧/提醒\"\n}\n\n### notes 里要写的技巧(1-3 条,短句)\n- 用年份/时期控制画风:year 2025 / newest / recent / mid / early / old\n- 用质量标签:masterpiece / best quality 或 score_9..score_1(可混用)\n- 角色外观要说清:发色发型、眼睛、服装、表情、姿态、镜头、光照、背景元素\n- 多人时逐个描述,避免只堆角色名\n- Tag dropout 存在:不必塞满所有标签,但关键标签必须有\n\n现在开始:根据用户输入生成 Anima 提示词 JSON。",
1,
2048
]
},
{
"id": 3,
"type": "SimpleChatPromptJsonUnpack",
"pos": [
-330.0,
360.0
],
"size": [
420,
300
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "json_text",
"type": "STRING",
"link": 2
}
],
"outputs": [
{
"name": "positive",
"type": "STRING",
"links": [
3
]
},
{
"name": "negative",
"type": "STRING",
"links": [
4
]
},
{
"name": "width",
"type": "INT",
"links": null
},
{
"name": "height",
"type": "INT",
"links": null
},
{
"name": "steps",
"type": "INT",
"links": null
},
{
"name": "cfg",
"type": "FLOAT",
"links": null
},
{
"name": "sampler",
"type": "SAMPLER",
"links": null
},
{
"name": "seed",
"type": "INT",
"links": null
},
{
"name": "notes",
"type": "STRING",
"links": [
5
]
},
{
"name": "vars",
"type": "SIMPLECHAT_VARS",
"links": null
},
{
"name": "obj",
"type": "SIMPLECHAT_JSON",
"links": null
},
{
"name": "sampler_name",
"type": "STRING",
"links": null
}
],
"properties": {
"aux_id": "Moeblack/ComfyUI-SimpleChat",
"Node name for S&R": "SimpleChatPromptJsonUnpack"
},
"widgets_values": [
"",
true
]
},
{
"id": 4,
"type": "PreviewAny",
"pos": [
160.0,
240.0
],
"size": [
300,
230
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "source",
"type": "*",
"link": 3
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"Node name for S&R": "PreviewAny"
},
"widgets_values": []
},
{
"id": 5,
"type": "PreviewAny",
"pos": [
160.0,
500.0
],
"size": [
300,
230
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "source",
"type": "*",
"link": 4
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"Node name for S&R": "PreviewAny"
},
"widgets_values": []
},
{
"id": 6,
"type": "SimpleChatMarkdownPreview",
"pos": [
160.0,
770.0
],
"size": [
320,
210
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 5
}
],
"outputs": [],
"properties": {
"aux_id": "Moeblack/ComfyUI-SimpleChat",
"Node name for S&R": "SimpleChatMarkdownPreview"
},
"widgets_values": [
""
]
}
],
"links": [
[
1,
1,
0,
2,
0,
"SIMPLECHAT_CONFIG"
],
[
2,
2,
0,
3,
0,
"STRING"
],
[
3,
3,
0,
4,
0,
"*"
],
[
4,
3,
1,
5,
0,
"*"
],
[
5,
3,
8,
6,
0,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {
"workflowRendererVersion": "LG",
"frontendVersion": "1.33.10"
},
"version": 0.4
}
+47
View File
@@ -0,0 +1,47 @@
"""
Template utilities for SimpleChat.
Currently provides a lightweight Mustache-style replacement:
- Replace {{ key }} with vars[key] (stringified).
- Unmatched placeholders are left as-is by default (easier debugging).
"""
from __future__ import annotations
import re
from typing import Any, Mapping
_MUSTACHE_RE = re.compile(r"\{\{\s*([^\{\}\n]+?)\s*\}\}")
def render_mustache(text: str, vars: Mapping[str, Any] | None, *, keep_unmatched: bool = True) -> str:
"""
Render {{var}} placeholders in text using vars mapping.
Args:
text: Input text (any string field).
vars: Mapping of variable name -> value. Values will be stringified.
keep_unmatched: If True, keep unknown placeholders like {{foo}} unchanged.
"""
if not isinstance(text, str) or not text:
return text
# Fast path: avoid regex unless needed
if "{{" not in text or "}}" not in text:
return text
if not vars or not isinstance(vars, Mapping):
return text
def _repl(match: re.Match) -> str:
key = match.group(1).strip()
if not key:
return match.group(0) if keep_unmatched else ""
if key in vars:
value = vars.get(key)
return "" if value is None else str(value)
return match.group(0) if keep_unmatched else ""
return _MUSTACHE_RE.sub(_repl, text)
+101
View File
@@ -0,0 +1,101 @@
# Anima(CircleStone Labs × Comfy Org)提示词生成模板(给 SimpleChat 用)
> 目标:让 LLM 帮你把“想画什么”转换成 **Anima 可用的 Danbooru 标签 + 自然语言混合提示词**,并附带推荐生成参数。
>
> 模型特点:偏二次元/插画,不擅长写真。建议 1MP 分辨率附近(如 1024×1024)。
---
## 1) 直接可用的 System Prompt(强制:只输出一个 JSON,便于工作流“拆字段”)
把下面内容粘贴到 `SimpleChat -> Chat` 节点的 `system` 输入里即可:
```text
你是“Anima(circlestone-labs/Anima)专用提示词工程师”,目标是在 ComfyUI 里用 Anima 生成高质量二次元/插画向图像(非写实、非摄影)。
### 模型规则(必须遵守)
1) 你必须输出“可直接粘贴到 ComfyUI”的提示词与参数建议(通过 JSON 字段给出)。
2) Anima 支持 Danbooru 风格 tags + 自然语言混合输入;输出时以 tags 为主,自然语言必须至少 2 句(多人/复杂构图时更要写清楚)。
3) Anima 标签顺序固定为:
[质量/元数据/年份/安全标签] [人物数量] [角色名] [作品名] [画师] [一般描述标签]
4) 画师标签必须以 @ 开头(例如 @xxx),否则影响很弱。
5) 默认生成参数(可按用户需求微调):
- 分辨率:约 1MP(如 1024x1024 / 896x1152 / 1152x896)
- Steps:30–50
- CFG:4–5
- Sampler:优先 er_sde;也可 euler_a;想更“发散/创意”可 dpmpp_2m_sde_gpu
6) 安全标签必须明确:safe / sensitive / nsfw / explicit;并在 negative 里加入相反约束(例如 positive 里是 safe,则 negative 要包含 nsfw, explicit)。
7) 不要追求写实:避免 photo, realistic, 真实摄影、皮肤毛孔等措辞;应强调 illustration, anime style, painterly, lineart 等。
8) 若用户想要非二次元风格:positive 第一行写 dataset tag(ye-pop 或 deviantart),换行后再给标题/描述,再给正常标签行。
### 输出格式(必须是 JSON,且不要夹带额外解释文字)
输出一个 JSON 对象,键固定如下:
{
"positive": "...",
"negative": "...",
"width": 1024,
"height": 1024,
"steps": 40,
"cfg": 4.5,
"sampler": "er_sde",
"seed": -1,
"notes": "1-3条简短技巧/提醒"
}
### 质量/年份/安全标签建议
- 质量:masterpiece / best quality 或 score_9~score_1(二选一或都用)
- 年份:year 2025 / newest / recent / mid / early / old
- 安全:safe / sensitive / nsfw / explicit(按用户要求)
### 负面提示词建议(按需挑选,不要堆太多)
worst quality, low quality, blurry, bad anatomy, bad hands, extra fingers, missing fingers, text, watermark, logo, jpeg artifacts
### notes 里要写的技巧(1-3 条,短句)
- 用年份/时期控制画风:year 2025 / newest / recent / mid / early / old
- 用质量标签:masterpiece / best quality 或 score_9..score_1(可混用)
- 角色外观要说清:发色发型、眼睛、服装、表情、姿态、镜头、光照、背景元素
- 多人时逐个描述,避免只堆角色名
- Tag dropout 存在:不必塞满所有标签,但关键标签必须有
现在开始:根据用户输入生成 Anima 提示词 JSON。
```
---
## 2) 用户输入(User Prompt)怎么写更出片
给 LLM 的用户输入建议包含这些维度(越具体越稳定):
- **题材**:角色设计/概念图/封面插画/立绘/表情包风格等
- **人物**:人数、性别、年龄段、发色/发型、服饰、表情、动作
- **构图**:半身/全身、仰视/俯视、景别、镜头焦段感(如“广角夸张透视”)
- **场景**:室内/室外、季节、时间、光照(柔光/逆光/霓虹)
- **风格**:赛璐璐/厚涂/水彩/线稿/2.5D 等
- **安全**:safe/nsfw/explicit(明确写)
- **可选:画师**:必须带 `@`
示例用户输入:
```text
画一张二次元角色设定插画:1girl,黑长直,红瞳,黑色风衣,手持雨伞,雨夜街头霓虹灯,赛璐璐+少量厚涂,情绪冷淡,半身构图。safe。画师参考:@xxx
```
---
## 3) 生成“非二次元艺术风格”的特殊用法
Anima 也支持额外数据集标签(放在第一行,换行后写标题/描述):
```text
ye-pop
For Sale: Others by Arun Prem
Abstract, oil painting of ...
```
或:
```text
deviantart
Flame
Digital painting of a fiery dragon ...
```
+87
View File
@@ -0,0 +1,87 @@
# Anima Prompt Scheme(JSON + 内置胡子变量)
本方案用于:**LLM 只输出一个 JSON** → ComfyUI 里用 `Prompt JSON Unpack` **一键拆字段** → 直接接入你的图像工作流(positive/negative/参数),同时自动得到一套可复用的 Mustache 变量。
---
## 1) LLM 必须输出的 JSON Schema(固定)
> 你在 `docs/anima_prompt.md` 的 System Prompt 已经强制要求输出这个结构。
```json
{
"positive": "...",
"negative": "...",
"width": 1024,
"height": 1024,
"steps": 40,
"cfg": 4.5,
"sampler": "er_sde",
"seed": -1,
"notes": "1-3条简短技巧/提醒"
}
```
---
## 2) `Prompt JSON Unpack` 的输出(端口)
- **positive/negative**: `STRING`
- **width/height/steps/seed**: `INT`
- **cfg**: `FLOAT`
- **sampler**: `SAMPLER`(对象,适配 custom-sampling 工作流)
- **sampler_name**: `STRING`(文本形式,便于展示/拼接/日志)
- **notes**: `STRING`
- **vars**: `SIMPLECHAT_VARS`(下文“内置胡子变量”)
- **obj**: `SIMPLECHAT_JSON`(完整 dict)
---
## 3) 内置胡子变量(Mustache Vars)
把 `Prompt JSON Unpack.vars` 接到任何 SimpleChat 节点的 `vars` 输入后,就能在 `prompt/system` 里直接使用这些变量:
### 3.1 推荐使用(带前缀,避免变量冲突)
- `{{anima.positive}}`
- `{{anima.negative}}`
- `{{anima.width}}`
- `{{anima.height}}`
- `{{anima.steps}}`
- `{{anima.cfg}}`
- `{{anima.sampler}}` / `{{anima.sampler_name}}`
- `{{anima.seed}}`
- `{{anima.notes}}`
### 3.2 简写(不带前缀)
- `{{positive}}`
- `{{negative}}`
- `{{width}}`
- `{{height}}`
- `{{steps}}`
- `{{cfg}}`
- `{{sampler}}` / `{{sampler_name}}`
- `{{seed}}`
- `{{notes}}`
### 3.3 中文别名(可选)
- `{{正面提示词}}` / `{{anima.正面提示词}}`
- `{{负面提示词}}` / `{{anima.负面提示词}}`
- `{{宽}}` / `{{anima.宽}}`
- `{{高}}` / `{{anima.高}}`
- `{{步数}}` / `{{anima.步数}}`
- `{{采样器}}` / `{{anima.采样器}}`
- `{{种子}}` / `{{anima.种子}}`
- `{{备注}}` / `{{anima.备注}}`
---
## 4) 最小工作流接线(推荐)
1) `SimpleChatText`(system 用 `docs/anima_prompt.md`,prompt 写你的需求)
2) 输出接 `Prompt JSON Unpack.json_text`
3) 把 `positive/negative/width/...` 分别接到你现有出图工作流对应口
4) 如需二次加工:把 `Prompt JSON Unpack.vars` 接到下一个 `SimpleChatText.vars`,就能用上面的胡子变量复用字段
+73
View File
@@ -0,0 +1,73 @@
# Anima(circlestone-labs/Anima)专用方案
目标:在 ComfyUI 里用 Anima 生成高质量二次元/插画向图像(非写实、非摄影)。
---
## 1) 强制 JSON 输出(推荐)
建议你在 `Chat.system` 中使用仓库提供的模板(已强制“只输出一个 JSON”):
- `docs/anima_prompt.md`
LLM 输出 JSON schema(固定):
```json
{
"positive": "...",
"negative": "...",
"width": 1024,
"height": 1024,
"steps": 40,
"cfg": 4.5,
"sampler": "er_sde",
"seed": -1,
"notes": "1-3条简短技巧/提醒"
}
```
---
## 2) 一键拆字段(避免“只能接 positive 不能接 json”)
把 LLM 的输出接到:
- `Prompt JSON Unpack.json_text`
你会得到强类型输出口(可直接接到 ComfyUI 节点):
- `positive/negative`(STRING)
- `width/height/steps/seed`(INT)
- `cfg`(FLOAT)
- `sampler`(SAMPLER 对象,用于 custom-sampling 图)
- `sampler_name`(STRING,便于展示/拼接)
- `notes`(STRING)
---
## 3) 内置胡子变量(vars)
`Prompt JSON Unpack` 同时输出 `vars`(`SIMPLECHAT_VARS`),可直接用于 Mustache 模板渲染:
推荐用带前缀(防冲突):
- `{{anima.positive}}` / `{{anima.negative}}`
- `{{anima.width}}` / `{{anima.height}}`
- `{{anima.steps}}` / `{{anima.cfg}}`
- `{{anima.sampler}}` / `{{anima.sampler_name}}`
- `{{anima.seed}}` / `{{anima.notes}}`
也支持不带前缀的简写:`{{positive}}`、`{{width}}` 等;并提供中文别名(如 `{{正面提示词}}`)。
更完整的变量清单见:
- `docs/anima_scheme.md`
---
## 4) 规则要点(给提示词工程)
- **标签顺序固定**:
`[质量/元数据/年份/安全] [人数] [角色名] [作品名] [画师] [通用标签]`
- **画师必须 @**:例如 `@xxx`
- **混合输入**:tags + 自然语言(自然语言建议至少 2 句,复杂构图更要写清楚)
- **默认参数**:约 1MP;steps 30–50;cfg 4–5;sampler 优先 `er_sde`
- **安全标签**:safe/sensitive/nsfw/explicit 必须明确;negative 里写相反约束
- **避免写实**:不要用 photo/realistic/皮肤毛孔等措辞
- **非二次元风格**:第一行写 `ye-pop` 或 `deviantart`,换行后再给标题/描述与正常标签行
+27
View File
@@ -0,0 +1,27 @@
# ComfyUI-SimpleChat Wiki
这份 Wiki 用于集中放置插件的使用文档与工作流方案。
> 维护方式:本仓库的 `docs/wiki/` 会通过 GitHub Actions **自动同步**到 GitHub Wiki(原生 Wiki)。
---
## 快速入口
- **节点清单与用途**:见 `Nodes.md`
- **Anima(circlestone-labs/Anima)专用方案**:见 `Anima.md`
- **Wiki 自动发布(配置/排错)**:见 `Wiki-Sync.md`
---
## 推荐工作流(Anima:LLM→JSON→拆字段→接入出图)
核心思路:
1) LLM **只输出一个固定 JSON**(包含 positive/negative/width/height/steps/cfg/sampler/seed/notes)
2) 用 `Prompt JSON Unpack` 把 JSON **拆成多个强类型端口**(STRING/INT/FLOAT/SAMPLER)
3) 把各字段直接接到 ComfyUI 的对应输入口(避免“只接收 positive 不接收 json”的问题)
4) 同时得到 `vars`(内置胡子变量)用于复用/二次加工
示例工作流文件:
- `assets/AnimaPromptScheme.json`
+27
View File
@@ -0,0 +1,27 @@
# Nodes(节点清单)
分类路径均为 `SimpleChat` 或 `SimpleChat/Utils`(右键菜单里可搜到)。
---
## SimpleChat(主节点)
- **API Config**:统一配置 OpenAI / Claude / Gemini(支持刷新模型列表)
- **Chat**:文本对话(支持 `system`)
- **Chat with Image**:图文对话
- **Chat NoASS**:NoASS 角色扮演模式(实验性)
- **Gemini Image Gen / Edit**:Gemini 原生文生图/图片编辑
> 以上节点均支持可选输入 `vars`:用于把 `{{变量}}` 模板渲染进 prompt/system 等文本字段。
---
## SimpleChat/Utils(工具节点)
- **Text Input**:纯文本输入框(输出 STRING)
- **Mustache Var**:定义变量(输出 `SIMPLECHAT_VARS` 字典,支持菊花链合并)
- **Mustache Render**:不经过 Chat,也能把 `{{变量}}` 渲染成纯文本(输出 STRING)
- **JSON Parse**:解析 JSON 并按 path 拆 8 路输出(支持 `a.b[0].c`、支持 ```json 代码块提取)
- **Prompt JSON Unpack**:固定 schema 的“一键拆包”(强类型输出 + 自动生成 `vars`)
- **Markdown Preview**:把文本以 Markdown 方式弹窗预览(已净化)
+33
View File
@@ -0,0 +1,33 @@
# Wiki Sync(GitHub 原生 Wiki 自动发布)
本仓库采用 **GitHub 原生 Wiki**,并使用 GitHub Actions 把 `docs/wiki/` 自动同步到 Wiki 仓库。
---
## 1) 需要在 GitHub 仓库里打开 Wiki 功能
进入 GitHub 仓库:
- Settings -> Features -> 勾选 **Wiki**
若未启用,Actions 会在克隆 `${repo}.wiki.git` 时失败。
---
## 2) 文档如何更新
请直接改动仓库内:
- `docs/wiki/*.md`
推送到 `main/master` 后会自动发布到 Wiki。
---
## 3) 同步工作流文件在哪里
自动发布配置位于:
- `.github/workflows/publish-wiki.yml`
同步逻辑:
- 把 `docs/wiki/` rsync 到 Wiki 仓库根目录
- 自动提交并 push
+12
View File
@@ -7,6 +7,12 @@ from .chat_image import SimpleChatImage
from .chat_noass import SimpleChatNoASS
from .gemini_gen import GeminiImageGen
from .gemini_edit import GeminiImageEdit
from .mustache_var import SimpleChatMustacheVar
from .mustache_render import SimpleChatMustacheRender
from .text_input import SimpleChatTextInput
from .json_parse import SimpleChatJsonParse
from .markdown_preview import SimpleChatMarkdownPreview
from .prompt_json_unpack import SimpleChatPromptJsonUnpack
__all__ = [
"SimpleChatConfig",
@@ -15,4 +21,10 @@ __all__ = [
"SimpleChatNoASS",
"GeminiImageGen",
"GeminiImageEdit",
"SimpleChatMustacheVar",
"SimpleChatMustacheRender",
"SimpleChatTextInput",
"SimpleChatJsonParse",
"SimpleChatMarkdownPreview",
"SimpleChatPromptJsonUnpack",
]
+7
View File
@@ -2,6 +2,7 @@
Chat node - Basic text conversation.
"""
from ..core import get_provider, ChatConfig
from ..core.template import render_mustache
class SimpleChatText:
@@ -16,6 +17,7 @@ class SimpleChatText:
},
"optional": {
"system": ("STRING", {"multiline": True, "default": ""}),
"vars": ("SIMPLECHAT_VARS",),
"temperature": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.1}),
"max_tokens": ("INT", {"default": 2048, "min": 1, "max": 128000}),
}
@@ -32,9 +34,14 @@ class SimpleChatText:
config: ChatConfig,
prompt: str,
system: str = "",
vars=None,
temperature: float = 1.0,
max_tokens: int = 2048,
):
# Template rendering ({{var}}) for prompt/system
prompt = render_mustache(prompt, vars)
system = render_mustache(system, vars)
# Build messages
messages = []
if system.strip():
+7
View File
@@ -3,6 +3,7 @@ Chat with Image node - Send image to LLM for analysis.
"""
import torch
from ..core import get_provider, ChatConfig
from ..core.template import render_mustache
class SimpleChatImage:
@@ -18,6 +19,7 @@ class SimpleChatImage:
},
"optional": {
"system": ("STRING", {"multiline": True, "default": ""}),
"vars": ("SIMPLECHAT_VARS",),
"temperature": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.1}),
"max_tokens": ("INT", {"default": 2048, "min": 1, "max": 128000}),
}
@@ -35,9 +37,14 @@ class SimpleChatImage:
image: torch.Tensor,
prompt: str,
system: str = "",
vars=None,
temperature: float = 1.0,
max_tokens: int = 2048,
):
# Template rendering ({{var}}) for prompt/system
prompt = render_mustache(prompt, vars)
system = render_mustache(system, vars)
# Build messages
messages = []
if system.strip():
+10
View File
@@ -11,6 +11,7 @@ from ..core import (
build_full_history,
get_stop_sequences,
)
from ..core.template import render_mustache
class SimpleChatNoASS:
@@ -39,6 +40,7 @@ class SimpleChatNoASS:
"placeholder": "Force the start of the assistant's response (Prefill)."
}),
"history": ("STRING", {"multiline": True, "default": "", "forceInput": True}),
"vars": ("SIMPLECHAT_VARS",),
"image": ("IMAGE",),
"user_name": ("STRING", {"default": "User"}),
"char_name": ("STRING", {"default": "Assistant"}),
@@ -60,12 +62,20 @@ class SimpleChatNoASS:
user_action: str,
prefill_start: str = "",
history: str = "",
vars=None,
image: torch.Tensor = None,
user_name: str = "User",
char_name: str = "Assistant",
temperature: float = 1.0,
max_tokens: int = 2048,
):
# Template rendering ({{var}}) for scenario/user/prefill
scenario_instructions = render_mustache(scenario_instructions, vars)
user_action = render_mustache(user_action, vars)
prefill_start = render_mustache(prefill_start, vars)
user_name = render_mustache(user_name, vars)
char_name = render_mustache(char_name, vars)
# Format in NoASS style
system_prompt, prefilled = format_noass_prompt(
system=scenario_instructions,
+6
View File
@@ -3,6 +3,7 @@ Gemini Image Edit node - Edit images using Gemini.
"""
import torch
from ..core import get_provider, ChatConfig
from ..core.template import render_mustache
class GeminiImageEdit:
@@ -17,6 +18,7 @@ class GeminiImageEdit:
"prompt": ("STRING", {"multiline": True, "default": ""}),
},
"optional": {
"vars": ("SIMPLECHAT_VARS",),
"size": (["1K", "2K", "4K"], {"default": "1K"}),
}
}
@@ -32,8 +34,12 @@ class GeminiImageEdit:
config: ChatConfig,
image: torch.Tensor,
prompt: str,
vars=None,
size: str = "1K",
):
# Template rendering ({{var}}) for prompt
prompt = render_mustache(prompt, vars)
# Validate provider
if config.provider != "gemini":
raise ValueError("Gemini Image Edit requires Gemini provider. Please use a Gemini config.")
+6
View File
@@ -3,6 +3,7 @@ Gemini Image Gen node - Text-to-image generation with Gemini.
"""
import torch
from ..core import get_provider, ChatConfig
from ..core.template import render_mustache
class GeminiImageGen:
@@ -16,6 +17,7 @@ class GeminiImageGen:
"prompt": ("STRING", {"multiline": True, "default": ""}),
},
"optional": {
"vars": ("SIMPLECHAT_VARS",),
"aspect_ratio": (["1:1", "16:9", "9:16", "4:3", "3:4", "5:4", "4:5"], {"default": "1:1"}),
"size": (["1K", "2K", "4K"], {"default": "1K"}),
}
@@ -31,9 +33,13 @@ class GeminiImageGen:
self,
config: ChatConfig,
prompt: str,
vars=None,
aspect_ratio: str = "1:1",
size: str = "1K",
):
# Template rendering ({{var}}) for prompt
prompt = render_mustache(prompt, vars)
# Validate provider
if config.provider != "gemini":
raise ValueError("Gemini Image Gen requires Gemini provider. Please use a Gemini config.")
+169
View File
@@ -0,0 +1,169 @@
"""
JSON parsing node for SimpleChat.
Parses a JSON string (optionally wrapped in ```json code fences) and extracts
up to 8 fields via simple dot/bracket paths.
"""
from __future__ import annotations
import json
import re
from typing import Any
_CODE_FENCE_RE = re.compile(r"```(?:json)?\s*([\s\S]*?)\s*```", re.IGNORECASE)
def _strip_code_fence(text: str) -> str:
if not isinstance(text, str):
return text
m = _CODE_FENCE_RE.search(text)
return m.group(1) if m else text
def _tokenize_path(path: str) -> list[Any]:
"""
Tokenize a simple path like:
a.b[0].c
items[0]['name']
items[0][\"name\"]
Returns tokens: ["a","b",0,"c"] ...
"""
if not isinstance(path, str):
return []
s = path.strip()
if not s:
return []
tokens: list[Any] = []
i = 0
n = len(s)
while i < n:
ch = s[i]
if ch == ".":
i += 1
continue
if ch == "[":
j = s.find("]", i + 1)
if j == -1:
# malformed, treat rest as a single token
tokens.append(s[i:])
break
inner = s[i + 1 : j].strip()
# quoted key
if (inner.startswith('"') and inner.endswith('"')) or (inner.startswith("'") and inner.endswith("'")):
tokens.append(inner[1:-1])
else:
try:
tokens.append(int(inner))
except Exception:
tokens.append(inner)
i = j + 1
continue
# normal key token until '.' or '['
j = i
while j < n and s[j] not in ".[":
j += 1
key = s[i:j].strip()
if key:
tokens.append(key)
i = j
return tokens
_MISSING = object()
def _get_by_tokens(obj: Any, tokens: list[Any]) -> Any:
cur = obj
for tok in tokens:
if isinstance(tok, int):
if isinstance(cur, (list, tuple)) and -len(cur) <= tok < len(cur):
cur = cur[tok]
else:
return _MISSING
else:
if isinstance(cur, dict) and tok in cur:
cur = cur[tok]
else:
return _MISSING
return cur
def _stringify(value: Any) -> str:
if value is None:
return ""
if isinstance(value, str):
return value
if isinstance(value, (int, float, bool)):
return str(value)
try:
return json.dumps(value, ensure_ascii=False)
except Exception:
return str(value)
class SimpleChatJsonParse:
"""
Parse a JSON string and split into multiple outputs (fixed 8 ports).
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"json_text": ("STRING", {"multiline": True, "default": "", "forceInput": True}),
"strip_code_fence": ("BOOLEAN", {"default": True}),
"default": ("STRING", {"default": ""}),
"path1": ("STRING", {"default": ""}),
"path2": ("STRING", {"default": ""}),
"path3": ("STRING", {"default": ""}),
"path4": ("STRING", {"default": ""}),
"path5": ("STRING", {"default": ""}),
"path6": ("STRING", {"default": ""}),
"path7": ("STRING", {"default": ""}),
"path8": ("STRING", {"default": ""}),
}
}
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "SIMPLECHAT_JSON")
RETURN_NAMES = ("out1", "out2", "out3", "out4", "out5", "out6", "out7", "out8", "obj")
FUNCTION = "parse"
CATEGORY = "SimpleChat/Utils"
DESCRIPTION = "Parse JSON and extract up to 8 values by dot/bracket paths."
def parse(
self,
json_text: str,
strip_code_fence: bool = True,
default: str = "",
path1: str = "",
path2: str = "",
path3: str = "",
path4: str = "",
path5: str = "",
path6: str = "",
path7: str = "",
path8: str = "",
):
raw = _strip_code_fence(json_text) if strip_code_fence else json_text
data = json.loads(raw) if isinstance(raw, str) else raw
paths = [path1, path2, path3, path4, path5, path6, path7, path8]
outs: list[str] = []
for p in paths:
p = (p or "").strip()
if not p:
outs.append(default)
continue
tokens = _tokenize_path(p)
val = _get_by_tokens(data, tokens)
outs.append(default if val is _MISSING else _stringify(val))
return (*outs, data)
+28
View File
@@ -0,0 +1,28 @@
"""
Markdown preview node.
Backend returns UI text; frontend JS renders markdown to HTML safely.
"""
from __future__ import annotations
class SimpleChatMarkdownPreview:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"multiline": True, "default": "", "forceInput": True}),
}
}
RETURN_TYPES = ()
FUNCTION = "preview"
OUTPUT_NODE = True
CATEGORY = "SimpleChat/Utils"
DESCRIPTION = "Render input text as Markdown (frontend-rendered)."
def preview(self, text: str):
# Use the standard 'ui.text' channel (same shape as PreviewAny)
return {"ui": {"text": (text or "",)}}
+41
View File
@@ -0,0 +1,41 @@
"""
Mustache text render node.
This node renders {{var}} placeholders in arbitrary text using SIMPLECHAT_VARS
without requiring any SimpleChat "main" chat/image nodes.
"""
from __future__ import annotations
from typing import Any
from ..core.template import render_mustache
class SimpleChatMustacheRender:
"""
Render a text template with {{var}} placeholders.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"multiline": True, "default": "", "forceInput": True}),
},
"optional": {
# Accept either vars dict (SIMPLECHAT_VARS) or a parsed JSON object (SIMPLECHAT_JSON)
"vars": ("SIMPLECHAT_VARS,SIMPLECHAT_JSON",),
"keep_unmatched": ("BOOLEAN", {"default": True}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = "render"
CATEGORY = "SimpleChat/Utils"
DESCRIPTION = "Render {{var}} placeholders in text using SIMPLECHAT_VARS."
def render(self, text: str, vars: Any = None, keep_unmatched: bool = True):
return (render_mustache(text, vars, keep_unmatched=keep_unmatched),)
+48
View File
@@ -0,0 +1,48 @@
"""
Mustache variables node - define {{var}} values.
"""
from __future__ import annotations
from typing import Any
class SimpleChatMustacheVar:
"""
Define a single Mustache variable and output a merged vars dict.
Usage:
- Set name/value in this node
- Connect output to other SimpleChat nodes via optional `vars` input
- Use {{name}} anywhere in prompts to inject the value
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"name": ("STRING", {"default": "自定义胡子变量"}),
"value": ("STRING", {"multiline": True, "default": ""}),
},
"optional": {
"vars_in": ("SIMPLECHAT_VARS",),
},
}
RETURN_TYPES = ("SIMPLECHAT_VARS",)
RETURN_NAMES = ("vars",)
FUNCTION = "set_var"
CATEGORY = "SimpleChat/Utils"
DESCRIPTION = "Define a {{var}} value for prompt templating."
def set_var(self, name: str, value: str, vars_in: Any = None):
merged = {}
if isinstance(vars_in, dict):
merged.update(vars_in)
key = (name or "").strip()
if key:
merged[key] = value
return (merged,)
+247
View File
@@ -0,0 +1,247 @@
"""
Prompt JSON unpacker (Anima-oriented schema).
This node is designed to take an LLM output that is *one JSON object only*:
{
"positive": "...",
"negative": "...",
"width": 1024,
"height": 1024,
"steps": 40,
"cfg": 4.5,
"sampler": "er_sde",
"seed": -1,
"notes": "..."
}
And split it into typed ComfyUI outputs, plus a SIMPLECHAT_VARS dict for
Mustache templating in other SimpleChat nodes.
"""
from __future__ import annotations
import json
import re
from typing import Any
_CODE_FENCE_RE = re.compile(r"```(?:json)?\s*([\s\S]*?)\s*```", re.IGNORECASE)
def _strip_code_fence(text: str) -> str:
if not isinstance(text, str):
return text
m = _CODE_FENCE_RE.search(text)
return m.group(1) if m else text
def _try_parse_json(text: str) -> Any:
"""
Try strict JSON first; if it fails, apply a minimal cleanup and retry.
"""
try:
return json.loads(text)
except Exception:
# Common LLM mistake: trailing commas
cleaned = re.sub(r",\s*([}\]])", r"\1", text)
return json.loads(cleaned)
def _as_int(value: Any, default: int) -> int:
if value is None:
return default
if isinstance(value, bool):
return int(value)
if isinstance(value, int):
return value
if isinstance(value, float):
return int(round(value))
if isinstance(value, str):
s = value.strip()
if not s:
return default
try:
return int(s)
except Exception:
try:
return int(float(s))
except Exception:
return default
return default
def _as_float(value: Any, default: float) -> float:
if value is None:
return default
if isinstance(value, bool):
return float(int(value))
if isinstance(value, (int, float)):
return float(value)
if isinstance(value, str):
s = value.strip()
if not s:
return default
try:
return float(s)
except Exception:
return default
return default
def _as_str(value: Any, default: str = "") -> str:
if value is None:
return default
if isinstance(value, str):
return value
try:
return json.dumps(value, ensure_ascii=False)
except Exception:
return str(value)
_SAMPLER_ALIASES = {
# community shorthand -> ComfyUI names
"euler_a": "euler_ancestral",
"euler-ancestral": "euler_ancestral",
"euler ancestral": "euler_ancestral",
}
def _normalize_sampler(name: str, default: str) -> str:
if not isinstance(name, str):
return default
s = name.strip()
if not s:
return default
low = s.lower()
return _SAMPLER_ALIASES.get(low, low)
class SimpleChatPromptJsonUnpack:
"""
Unpack a fixed-schema JSON (positive/negative/params/notes) into typed outputs.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"json_text": ("STRING", {"multiline": True, "default": "", "forceInput": True}),
},
"optional": {
"strip_code_fence": ("BOOLEAN", {"default": True}),
},
}
RETURN_TYPES = (
"STRING", # positive
"STRING", # negative
"INT", # width
"INT", # height
"INT", # steps
"FLOAT", # cfg
"SAMPLER", # sampler
"INT", # seed
"STRING", # notes
"SIMPLECHAT_VARS", # vars
"SIMPLECHAT_JSON", # obj
"STRING", # sampler_name (string)
)
RETURN_NAMES = (
"positive",
"negative",
"width",
"height",
"steps",
"cfg",
"sampler",
"seed",
"notes",
"vars",
"obj",
"sampler_name",
)
FUNCTION = "unpack"
CATEGORY = "SimpleChat/Utils"
DESCRIPTION = "Unpack an LLM-produced JSON into ComfyUI-typed fields + Mustache vars."
def unpack(self, json_text: str, strip_code_fence: bool = True):
raw = _strip_code_fence(json_text) if strip_code_fence else json_text
data = _try_parse_json(raw) if isinstance(raw, str) else raw
if not isinstance(data, dict):
raise ValueError("Expected a JSON object (dict) at top-level.")
positive = _as_str(data.get("positive", ""), "")
negative = _as_str(data.get("negative", ""), "")
width = _as_int(data.get("width"), 1024)
height = _as_int(data.get("height"), 1024)
steps = _as_int(data.get("steps"), 40)
cfg = _as_float(data.get("cfg"), 4.5)
sampler_name = _normalize_sampler(_as_str(data.get("sampler", ""), ""), "er_sde")
seed = _as_int(data.get("seed"), -1)
notes = _as_str(data.get("notes", ""), "")
vars_dict = {
# English keys (match JSON schema)
"positive": positive,
"negative": negative,
"width": width,
"height": height,
"steps": steps,
"cfg": cfg,
"sampler": sampler_name,
"sampler_name": sampler_name,
"seed": seed,
"notes": notes,
# Prefixed scheme keys (avoid collisions)
"anima.positive": positive,
"anima.negative": negative,
"anima.width": width,
"anima.height": height,
"anima.steps": steps,
"anima.cfg": cfg,
"anima.sampler": sampler_name,
"anima.sampler_name": sampler_name,
"anima.seed": seed,
"anima.notes": notes,
# Chinese aliases (optional convenience)
"正面提示词": positive,
"负面提示词": negative,
"宽": width,
"高": height,
"步数": steps,
"采样器": sampler_name,
"种子": seed,
"备注": notes,
# Chinese aliases (prefixed)
"anima.正面提示词": positive,
"anima.负面提示词": negative,
"anima.宽": width,
"anima.高": height,
"anima.步数": steps,
"anima.采样器": sampler_name,
"anima.种子": seed,
"anima.备注": notes,
}
# Build an actual sampler object for SAMPLER output (for custom-sampling graphs)
import comfy.samplers
sampler_obj = comfy.samplers.sampler_object(sampler_name)
return (
positive,
negative,
width,
height,
steps,
cfg,
sampler_obj,
seed,
notes,
vars_dict,
data,
sampler_name,
)
+27
View File
@@ -0,0 +1,27 @@
"""
Simple text input node.
Provides a standalone multiline text box and outputs a STRING.
"""
from __future__ import annotations
class SimpleChatTextInput:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"multiline": True, "default": ""}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = "output"
CATEGORY = "SimpleChat/Utils"
DESCRIPTION = "Standalone multiline text input."
def output(self, text: str):
return (text,)
+187
View File
@@ -0,0 +1,187 @@
import { app } from "../../scripts/app.js";
const EXT_NAME = "ComfyUI.SimpleChat.MarkdownPreview";
const STYLE_ID = "simplechat-markdown-preview-style";
function chainCallback(object, property, callback) {
if (!object) return;
if (property in object && object[property]) {
const callback_orig = object[property];
object[property] = function () {
const r = callback_orig.apply(this, arguments);
return callback.apply(this, arguments) ?? r;
};
} else {
object[property] = callback;
}
}
function ensureStyles() {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement("style");
style.id = STYLE_ID;
style.type = "text/css";
style.innerHTML = `
.simplechat-md-modal {
position: fixed;
right: 18px;
top: 18px;
width: min(720px, calc(100vw - 36px));
height: min(70vh, 760px);
background: var(--comfy-menu-bg);
color: var(--fg-color);
border: 1px solid var(--border-color);
border-radius: 12px;
z-index: 9999;
box-shadow: 0 16px 40px rgba(0,0,0,0.35);
overflow: hidden;
display: flex;
flex-direction: column;
}
.simplechat-md-modal header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
border-bottom: 1px solid var(--border-color);
background: rgba(0,0,0,0.08);
font: 12px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
.simplechat-md-modal header .title {
opacity: 0.9;
}
.simplechat-md-modal header .actions {
display: flex;
gap: 8px;
align-items: center;
}
.simplechat-md-modal header button {
border: 1px solid var(--border-color);
background: transparent;
color: var(--fg-color);
border-radius: 8px;
padding: 4px 8px;
cursor: pointer;
font-size: 12px;
}
.simplechat-md-modal header button:hover {
background: rgba(255,255,255,0.06);
}
.simplechat-md-body {
padding: 12px;
overflow: auto;
}
.simplechat-md-body pre, .simplechat-md-body code {
background: rgba(0,0,0,0.16);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 8px;
padding: 8px 10px;
overflow: auto;
}
.simplechat-md-body a {
color: #ffd166;
}
.simplechat-md-body a:visited {
color: #f4a261;
}
`;
document.head.appendChild(style);
}
function loadScript(url) {
return new Promise((resolve, reject) => {
const existing = document.querySelector(`script[src="${url}"]`);
if (existing) return resolve(true);
const el = document.createElement("script");
el.async = true;
el.type = "text/javascript";
el.src = url;
el.addEventListener("load", () => resolve(true));
el.addEventListener("error", () => reject(new Error(`Failed to load ${url}`)));
document.body.appendChild(el);
});
}
let _libsPromise = null;
async function ensureMarkdownLibs() {
if (window.marked && window.DOMPurify) return;
if (_libsPromise) return _libsPromise;
const markedUrl = new URL("./vendor/marked.min.js", import.meta.url).href;
const purifyUrl = new URL("./vendor/purify.min.js", import.meta.url).href;
_libsPromise = (async () => {
await loadScript(markedUrl);
await loadScript(purifyUrl);
})();
return _libsPromise;
}
function escapeHtml(text) {
return (text || "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
let modalEl = null;
function showMarkdown(rawText, title = "Markdown Preview") {
ensureStyles();
if (!modalEl) {
modalEl = document.createElement("div");
modalEl.className = "simplechat-md-modal";
modalEl.innerHTML = `
<header>
<div class="title"></div>
<div class="actions">
<button data-action="copy">Copy</button>
<button data-action="close">Close</button>
</div>
</header>
<div class="simplechat-md-body"></div>
`;
document.body.appendChild(modalEl);
modalEl.querySelector('button[data-action="close"]').addEventListener("click", () => {
modalEl.remove();
modalEl = null;
});
modalEl.querySelector('button[data-action="copy"]').addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(modalEl.__rawText || "");
} catch (e) {
console.warn("Clipboard copy failed", e);
}
});
}
modalEl.__rawText = rawText || "";
modalEl.querySelector(".title").textContent = title;
const body = modalEl.querySelector(".simplechat-md-body");
const hasLibs = window.marked && window.DOMPurify;
if (hasLibs) {
const html = window.DOMPurify.sanitize(window.marked.parse(rawText || ""));
body.innerHTML = html;
} else {
body.innerHTML = `<pre>${escapeHtml(rawText || "")}</pre>`;
}
}
app.registerExtension({
name: EXT_NAME,
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData?.name !== "SimpleChatMarkdownPreview") return;
// Ensure libraries are loaded once; if it fails, we fall back to plain text
ensureMarkdownLibs().catch((e) => console.warn("[SimpleChat] markdown libs load failed:", e));
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, [message]);
const text = (message?.text && message.text.length) ? message.text[0] : "";
showMarkdown(text, "SimpleChat Markdown Preview");
};
},
});
+6
View File
File diff suppressed because one or more lines are too long
+3
View File
File diff suppressed because one or more lines are too long