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:
@@ -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
@@ -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/
|
||||
|
||||
@@ -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.**
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ...
|
||||
```
|
||||
|
||||
@@ -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`,就能用上面的胡子变量复用字段
|
||||
|
||||
@@ -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`,换行后再给标题/描述与正常标签行
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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 方式弹窗预览(已净化)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 "",)}}
|
||||
|
||||
@@ -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),)
|
||||
|
||||
@@ -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,)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,)
|
||||
|
||||
@@ -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("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
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");
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Vendored
+6
File diff suppressed because one or more lines are too long
Vendored
+3
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user