优化节点
This commit is contained in:
@@ -109,6 +109,10 @@ Both a **local model** and an **online API** backend are supported; API keys liv
|
||||
- **🖼️ Image Prompt Preset Pro** — the same, plus built-in LLM prompt enhancement / reverse.
|
||||
- **🖼️ Qwen2.1 Prompt Preset** — preset + element panel, wired to the official Load CLIP + Generate Text chain.
|
||||
- **🖌️ ShenBi** — an input-less paint board (create / edit modes) that outputs a strictly on-resolution image.
|
||||
> The preview window reads files from **this server's** `input` dir (boards live in `input/xb_shenbi/`).
|
||||
> A workflow only stores the file name, so after copying it to another machine (local ↔ cloud) the window
|
||||
> falls back to a blank canvas and explains why in the badge / browser console. The window itself needs a
|
||||
> recent ComfyUI frontend (DOM widgets).
|
||||
|
||||
#### 9. 🖼️ Model-specific Encoders (3)
|
||||
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
- **🖼️ 生图提示词预设 Pro**:同上,并内置 LLM 提示词增强 / 反推。
|
||||
- **🖼️ Qwen2.1 提示词预设**:预设 + 元素面板,接官方 Load CLIP + Generate Text 链路。
|
||||
- **🖌️ 神笔**:无输入端、只输出 Image 的画板节点(创作 / 编辑双模式),输出严格对齐设定分辨率。
|
||||
> 显示窗依赖**本机** input 目录里的文件(画板存在 `input/xb_shenbi/`):工作流只记文件名,
|
||||
> 把工作流拷到别的机器(本地 ↔ 云机)后,除非那台机器上有同名文件,否则显示窗会回落成空白画布
|
||||
> 并在角标 / 浏览器控制台说明原因。显示窗本身需要较新的 ComfyUI 前端(支持 DOM 部件)。
|
||||
|
||||
#### 9. 🖼️ 专用编码器(3 个)
|
||||
|
||||
|
||||
@@ -3249,6 +3249,8 @@ function setupNode(node) {
|
||||
* + 提示词框标题行(右侧「📋 复制」)+ 参数设定显示(只读摘要 5 行)
|
||||
* + 「📝 提示词」端口接线 → 提示词框锁定(二选一)
|
||||
* 弹窗:导演台同款二级弹窗(底部:自动保存 + 字号 + 界面缩放 + 取消/💾 保存)
|
||||
* 🤖 LLM设置 顶部 = LLM 增强总开关(单选必选:启用LLM增强 / 关闭LLM增强,默认启用)
|
||||
* → 节点 widget `llm_enable`(BOOLEAN,节点表面隐藏);关闭 = 后端不加载文本编码器、不生成文本
|
||||
* 配置:manager_settings.qwen = { run.seed_control, extra_settings }(Generate Text 的全量参数走节点 widget,由弹窗读写)
|
||||
* 说明:本块所有标识符统一 xbr/XBR 前缀,避免与生图预设面板既有函数/常量重名。
|
||||
* ========================================================================== */
|
||||
@@ -3258,12 +3260,15 @@ const XBR_SEED_MODES = ["randomize", "fixed", "increment", "decrement"];const XB
|
||||
const XBR_IO_MODES = ["自动", "文生图", "图生图"];
|
||||
const XBR_SAMPLING_MODES = ["on", "off"];
|
||||
const XBR_MTP_MODES = ["auto", "off", "2", "3", "4", "5"];
|
||||
/** LLM 增强总开关(🤖 LLM设置 弹窗顶部单选,必选;默认「启用LLM增强」)*/
|
||||
const XBR_LLM_MODES = ["启用LLM增强", "关闭LLM增强"];
|
||||
const XBR_PANEL_BUTTONS = [
|
||||
{ id: "llm", label: "🤖 LLM设置", title: "🤖 LLM设置", subtitle: "模型选择 = 官方 Load CLIP;参数设置 = 官方 Generate Text 全量参数。执行节点即生成,没有开关。" },
|
||||
{ id: "llm", label: "🤖 LLM设置", title: "🤖 LLM设置", subtitle: "顶部 = LLM 增强总开关(默认启用);模型选择 = 官方 Load CLIP;参数设置 = 官方 Generate Text 全量参数。" },
|
||||
{ id: "preset", label: "✨ 预设参数", title: "✨ 预设参数", subtitle: "输出语言 / 空latent类型 / 预设模式 / 设定词 / SKILL。提示词正文在节点上的提示词框里编辑。" },
|
||||
];
|
||||
/** 全部收进弹窗的节点 widget(节点表面只留画幅参数 / 生成数量) */
|
||||
const XBR_QWEN_WIDGETS = [
|
||||
"llm_enable",
|
||||
"clip_name", "clip_type", "device",
|
||||
"max_length", "sampling_mode", "temperature", "top_k", "top_p", "min_p",
|
||||
"repetition_penalty", "presence_penalty", "seed", "control_after_generate",
|
||||
@@ -3497,7 +3502,18 @@ function xbrDialog({ label, title, subtitle, width = 820, autoSaveRef = null, on
|
||||
function xbrRenderLlm(body, node, draft, ctx) {
|
||||
const s = draft.settings.qwen;
|
||||
|
||||
body.append(makeSectionTitle("模型选择(官方 Load CLIP)"));
|
||||
// ── 顶部:LLM 增强总开关(单选必选,默认「启用LLM增强」)──
|
||||
body.append(makeSectionTitle("LLM 增强"));
|
||||
body.append(field("LLM 增强", radioRow(XBR_LLM_MODES, draft.llm ? XBR_LLM_MODES[0] : XBR_LLM_MODES[1], (v) => {
|
||||
draft.llm = (v === XBR_LLM_MODES[0]);
|
||||
ctx.rerender(); // 关闭 → 下面的区块标成不生效
|
||||
})));
|
||||
body.append(xbrHint(draft.llm
|
||||
? "启用(默认):执行节点时加载文本编码器,把正文生成成完整提示词(下面的模型与参数全部生效)。"
|
||||
: "关闭:执行节点只输出拼装好的提示词 + 空latent —— 不加载文本编码器、不生成文本(下面的模型与参数都不生效)。"));
|
||||
|
||||
const offMark = draft.llm ? "" : " — 已关闭 LLM 增强,不生效";
|
||||
body.append(makeSectionTitle("模型选择(官方 Load CLIP)" + offMark));
|
||||
const clips = xbrWidgetOptions(node, "clip_name");
|
||||
if (clips.length) body.append(field("模型", selectControl(clips, draft.clip_name, (v) => { draft.clip_name = v; })));
|
||||
else body.append(field("模型", xbrTextControl(draft.clip_name, "models/text_encoders 里没有模型", (v) => { draft.clip_name = v; })));
|
||||
@@ -3505,7 +3521,7 @@ function xbrRenderLlm(body, node, draft, ctx) {
|
||||
body.append(field("设备", selectControl(xbrWidgetOptions(node, "device"), draft.device, (v) => { draft.device = v; })));
|
||||
body.append(xbrHint("与官方 Load CLIP 完全一致:模型来自 ComfyUI/models/text_encoders;Qwen-Image 系文本编码器请选 类型 = qwen_image。"));
|
||||
|
||||
body.append(makeSectionTitle("参数设置(官方 Generate Text 全量参数)"));
|
||||
body.append(makeSectionTitle("参数设置(官方 Generate Text 全量参数)" + offMark));
|
||||
const t = draft.text;
|
||||
body.append(field("最大长度 max_length", xbrNumberControl(t.max_length, { min: 1, max: 32768, step: 1 }, (v) => { t.max_length = Math.round(v); })));
|
||||
body.append(field("采样模式 sampling_mode", selectControl(XBR_SAMPLING_MODES, t.sampling_mode, (v) => { t.sampling_mode = v; ctx.rerender(); })));
|
||||
@@ -3527,7 +3543,10 @@ function xbrRenderLlm(body, node, draft, ctx) {
|
||||
body.append(field("使用内置模板 use_default_template", checkboxControl(t.use_default_template, "使用模型自带的系统提示词 / 对话模板", (v) => { t.use_default_template = v; })));
|
||||
body.append(field("mtp", selectControl(XBR_MTP_MODES, t.mtp, (v) => { t.mtp = v; })));
|
||||
body.append(xbrHint("mtp:多 token 预测投机解码,没有 MTP 权重时无效;与官方 Generate Text 同义。"));
|
||||
body.append(el("div", "font-size:11px;color:#888;line-height:1.6;margin:2px 0 8px;", "提示词正文在节点上的提示词框里编辑;「📝 提示词」端口接线后该框锁定。"));
|
||||
body.append(el("div", "font-size:11px;color:" + (draft.llm ? "#888" : "#d9a441") + ";line-height:1.6;margin:2px 0 8px;",
|
||||
draft.llm
|
||||
? "提示词正文在节点上的提示词框里编辑;「📝 提示词」端口接线后该框锁定。"
|
||||
: "已关闭 LLM 增强:执行节点不会加载上面的模型、也不会用这些参数,输出 = 本档设定词置顶 + 提示词框正文(「📝 提示词」端口接线时用它)。"));
|
||||
}
|
||||
|
||||
/* ── 弹窗内容:✨ 预设参数(生图预设 + 设定词 + SKILL)───────────── */
|
||||
@@ -3685,7 +3704,8 @@ function xbrRenderHelp(body) {
|
||||
" · 第 2 行按钮:🎨 风格 | 📐 视角 | 👤 主体 | 🕺 姿态 | 👗 装扮 | 🎒 道具 | 💡 光影 | 🏞️ 背景",
|
||||
" · 画幅比例 / 宽度 / 高度 / 生成数量:按空latent类型的官方步长自动对齐",
|
||||
" · 提示词框右上角 📋 复制;下面是参数设定显示,实时告诉你当前配置",
|
||||
" · 没有开关:执行节点就会用文本编码器生成",
|
||||
" · 开关:🤖 LLM设置 顶部「启用LLM增强 / 关闭LLM增强」(默认启用);",
|
||||
" 关闭后执行节点只用拼装好的提示词,不加载文本编码器、不生成文本",
|
||||
"",
|
||||
"【元素面板怎么用】",
|
||||
" · 点选项行 = 加入提示词,再点一下 = 移除",
|
||||
@@ -3695,6 +3715,10 @@ function xbrRenderHelp(body) {
|
||||
" · 搜索框与「全部加入」只作用于当前子类",
|
||||
"",
|
||||
"【🤖 LLM设置】",
|
||||
" · 顶部总开关(单选必选,默认「启用LLM增强」):",
|
||||
" ◆ 启用LLM增强 = 执行节点时加载文本编码器,把正文生成成完整提示词",
|
||||
" ◆ 关闭LLM增强 = 只输出拼装好的提示词 + 空latent(不加载文本编码器、不生成文本)",
|
||||
" 下面的模型选择与参数全部不生效(面板里会标出「不生效」)",
|
||||
" · 模型选择 = 官方 Load CLIP:模型 / 类型 / 设备,枚举与官方节点逐字一致",
|
||||
" · 参数设置 = 官方 Generate Text 全量参数:max_length | sampling_mode | temperature |",
|
||||
" top_k | top_p | min_p | repetition_penalty | presence_penalty | seed |",
|
||||
@@ -3780,6 +3804,7 @@ function xbrRenderHelp(body) {
|
||||
" (多 GPU 机器上核显常被枚举成 cuda:0)→ 启动前设 HIP_VISIBLE_DEVICES=1 让 ComfyUI 用独显再试",
|
||||
" · 生成慢 / 显存不够:设备改 cpu,或把 max_length 调小",
|
||||
" · 结果太发散:温度调低、top_p 调小;想复现同一结果就把生成后控制改成固定",
|
||||
" · 只想拼提示词(不跑模型、省显存):🤖 LLM设置 顶部切成「关闭LLM增强」再执行",
|
||||
" · 节点表面参数很少?故意的:选项都收在弹窗里",
|
||||
" · 改完参数要重新执行节点才生效",
|
||||
"",
|
||||
@@ -3819,6 +3844,7 @@ async function xbrOpenModal(node, panelId) {
|
||||
skill: String(xbrWidgetVal(node, "skill_name") ?? ""),
|
||||
skillMode: (() => { const v = String(xbrWidgetVal(node, "skill_mode") ?? ""); return XBR_SKILL_MODES.includes(v) ? v : "自动"; })(),
|
||||
// 官方 Load CLIP
|
||||
llm: xbrFlag(xbrWidgetVal(node, "llm_enable"), true), // LLM 增强总开关(默认启用)
|
||||
clip_name: String(xbrWidgetVal(node, "clip_name") ?? ""),
|
||||
clip_type: String(xbrWidgetVal(node, "clip_type") ?? "qwen_image"),
|
||||
device: String(xbrWidgetVal(node, "device") ?? "default"),
|
||||
@@ -3855,6 +3881,7 @@ async function xbrOpenModal(node, panelId) {
|
||||
node.__ippWritePreset?.(draft.mode, ioCommit, draft.lang, draft.presetText);
|
||||
} catch (_) {}
|
||||
// 官方 Load CLIP
|
||||
xbrSetWidget(node, "llm_enable", draft.llm); // LLM 增强总开关
|
||||
xbrSetWidget(node, "clip_name", draft.clip_name, true);
|
||||
xbrSetWidget(node, "clip_type", draft.clip_type, true);
|
||||
xbrSetWidget(node, "device", draft.device, true);
|
||||
@@ -3931,11 +3958,14 @@ function xbrInfoLines(node, promptText) {
|
||||
const model = xbrClean(xbrWidgetVal(node, "clip_name"));
|
||||
const modelShort = String(model).split(/[\\/]/).pop() || model; // 只留文件名,不带目录
|
||||
const line1 = `📊 空 latent:${xbrClean(xbrWidgetVal(node, "latent_kind"))} | ${xbrWidgetVal(node, "width")}x${xbrWidgetVal(node, "height")} | 数量 ${xbrWidgetVal(node, "batch_size")}`;
|
||||
const line2 = `🤖 LLM 模型:${modelShort || "未选择"}`;
|
||||
const line3 = `⚙️ Text 参数: 最大长度 ${xbrWidgetVal(node, "max_length")} 丨 思考模式 ${xbrFlag(xbrWidgetVal(node, "thinking"), false) ? "开" : "关"} 丨 温度 ${xbrWidgetVal(node, "temperature")}`;
|
||||
const llmOn = xbrFlag(xbrWidgetVal(node, "llm_enable"), true);
|
||||
const line2 = llmOn ? `🤖 LLM 增强:已启用 | 模型:${modelShort || "未选择"}`
|
||||
: "🤖 LLM 增强:已关闭(只输出拼装好的提示词)";
|
||||
const line3 = `⚙️ Text 参数: 最大长度 ${xbrWidgetVal(node, "max_length")} 丨 思考模式 ${xbrFlag(xbrWidgetVal(node, "thinking"), false) ? "开" : "关"} 丨 温度 ${xbrWidgetVal(node, "temperature")}`
|
||||
+ (llmOn ? "" : "(不生效)");
|
||||
const ctlRaw = xbrClean(xbrWidgetVal(node, XBR_SEED_CTL_WIDGET)) || "randomize";
|
||||
const line4 = `✨ 生成种子:${xbrWidgetVal(node, "seed")} | ${XBR_SEED_LABEL[ctlRaw] || ctlRaw}`;
|
||||
const line5 = `📝 文本数量:${genLen} 字`;
|
||||
const line5 = llmOn ? `📝 文本数量:${genLen} 字` : "📝 文本数量:未生成(已关闭 LLM 增强)";
|
||||
return [line1, line2, line3, line4, line5].join("\n");
|
||||
}
|
||||
|
||||
|
||||
+139
-27
@@ -9,6 +9,14 @@
|
||||
* 创作模式 —— 没有画板存档时显示「设定分辨率 + 画布底色」的纯色画布(默认纯白);
|
||||
* 编辑模式 —— 显示画板存档,没画过则显示源图;
|
||||
* 画过之后两个模式都显示画板存档。
|
||||
* ⚠️ 显示窗要显示「存档 / 源图」时必须满足两条**服务端本机**条件:
|
||||
* ① 该文件存在于这台机器的 ComfyUI/input(画板在 input/xb_shenbi/);
|
||||
* ② /api/view 能取到它。
|
||||
* 工作流只带**文件名**,画板文件不会跟着工作流走 —— 从本地拷到云机、或云端 input 被重置时,
|
||||
* 文件不存在(/view 404),旧版本就是一块「图裂」黑窗(用户报的「云机上看不到画布」)。
|
||||
* 现在前端会把加载失败的 URL 记下来,回落成空白画布 / 源图,并在角标 + 控制台说明原因。
|
||||
* ⚠️ 显示窗是 DOM 部件(LGraphNode.addDOMWidget):ComfyUI 前端太旧(无该 API)时整块窗口都不出现,
|
||||
* 此时会在控制台报错并弹提示(提示升级前端)。
|
||||
* · 显示窗只做显示,不做点击编辑(避免和操作按钮重复成两个编辑入口);
|
||||
* 官方 image_upload 自带的 `upload` 按钮按模式显隐,`$$canvas-image-preview` 面板一律隐藏,
|
||||
* 保证「显示窗」全节点只有一个。
|
||||
@@ -185,6 +193,11 @@ function isPainting(value) {
|
||||
return PAINT_EXT_RE.test(text) && !/[\n\r]/.test(text);
|
||||
}
|
||||
|
||||
/** 存档值的短标签:老工作流里的内嵌 base64 太长,提示里只写"内嵌画像存档" */
|
||||
function archiveLabel(value) {
|
||||
return String(value || "").startsWith("data:") ? "内嵌画像存档" : String(value || "");
|
||||
}
|
||||
|
||||
/** 把画布 PNG 传到 input/xb_shenbi/ 下,返回节点里要存的文件名("子目录/文件名") */
|
||||
async function uploadPainting(blob, filename) {
|
||||
const body = new FormData();
|
||||
@@ -263,27 +276,36 @@ async function openBrushEditor(cfg) {
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
let bitmap = null;
|
||||
try {
|
||||
if (cfg.paintedUrl) {
|
||||
bitmap = await loadBitmap(cfg.paintedUrl);
|
||||
width = bitmap.naturalWidth;
|
||||
height = bitmap.naturalHeight;
|
||||
} else if (cfg.imageUrl) {
|
||||
bitmap = await loadBitmap(cfg.imageUrl);
|
||||
const sw = bitmap.naturalWidth;
|
||||
const sh = bitmap.naturalHeight;
|
||||
// 编辑模式:大于设定分辨率才缩小裁切,小于则保持原尺寸
|
||||
if (cfg.targetW && cfg.targetH && (sw > cfg.targetW || sh > cfg.targetH)) {
|
||||
width = cfg.targetW;
|
||||
height = cfg.targetH;
|
||||
// 底图候选:画板存档 → 源图。⚠️ 两者都是**服务端本机**的文件,工作流从别的机器
|
||||
// (如本地 → 云机)拷过来时常常读不到:读不到就退到下一个候选,都读不到按设定分辨率的
|
||||
// 空白画布打开 —— 绝不因为"存档不在本机"就把用户堵在"载入失败"上(后端也是这个兜底口径)。
|
||||
const candidates = [];
|
||||
if (cfg.paintedUrl) candidates.push({ url: cfg.paintedUrl, painted: true });
|
||||
if (cfg.imageUrl) candidates.push({ url: cfg.imageUrl, painted: false });
|
||||
for (const cand of candidates) {
|
||||
try {
|
||||
bitmap = await loadBitmap(cand.url);
|
||||
if (cand.painted) {
|
||||
width = bitmap.naturalWidth;
|
||||
height = bitmap.naturalHeight;
|
||||
} else {
|
||||
width = sw;
|
||||
height = sh;
|
||||
const sw = bitmap.naturalWidth;
|
||||
const sh = bitmap.naturalHeight;
|
||||
// 编辑模式:大于设定分辨率才缩小裁切,小于则保持原尺寸
|
||||
if (cfg.targetW && cfg.targetH && (sw > cfg.targetW || sh > cfg.targetH)) {
|
||||
width = cfg.targetW;
|
||||
height = cfg.targetH;
|
||||
} else {
|
||||
width = sw;
|
||||
height = sh;
|
||||
}
|
||||
}
|
||||
break;
|
||||
} catch (err) {
|
||||
bitmap = null;
|
||||
notify((cand.painted ? "画板存档" : "源图") + "在本机读不到,已跳过(文件不在服务端 input 目录):"
|
||||
+ ((err && err.message) || err), "warn");
|
||||
}
|
||||
} catch (err) {
|
||||
notify("图片载入失败:" + ((err && err.message) || err), "error");
|
||||
return null;
|
||||
}
|
||||
if (!width || !height) { width = cfg.width || 1024; height = cfg.height || 1024; }
|
||||
|
||||
@@ -756,7 +778,11 @@ function setupNode(node) {
|
||||
let panelImg = null;
|
||||
let panelHint = null;
|
||||
let panelCaption = null;
|
||||
let panelNote = null;
|
||||
let panelBox = null;
|
||||
/** 本机加载失败的图 URL(存档 / 源图文件不在服务端本机时最常见)——
|
||||
记住它,显示窗就不再拿它去撞墙,改为回落显示 + 说明 */
|
||||
const badSrcs = new Set();
|
||||
|
||||
/* ── 按钮:打开画板(放在显示窗上面) ─────────────────────── */
|
||||
const openBoard = async () => {
|
||||
@@ -881,12 +907,42 @@ function setupNode(node) {
|
||||
panelCaption = document.createElement("span");
|
||||
panelCaption.style.cssText = "position:absolute;left:10px;top:10px;padding:4px 11px;border-radius:999px;" +
|
||||
"background:rgba(20,20,25,.78);color:#cfd5df;font-size:12px;line-height:1;pointer-events:none;display:none;";
|
||||
box.append(panelImg, panelHint, panelCaption);
|
||||
// 存档读不到时的说明角标(压在左下角:纯白/纯黑画布上都看得清)
|
||||
panelNote = document.createElement("span");
|
||||
panelNote.style.cssText = "position:absolute;left:10px;bottom:10px;max-width:calc(100% - 20px);padding:7px 12px;" +
|
||||
"border-radius:8px;background:rgba(58,31,31,.94);border:1px solid #7d3b3b;color:#ffd9d9;" +
|
||||
"font-size:12px;line-height:1.6;pointer-events:none;display:none;white-space:pre-wrap;text-align:left;" +
|
||||
"box-sizing:border-box;overflow:hidden;";
|
||||
box.append(panelImg, panelHint, panelCaption, panelNote);
|
||||
panelBox = box;
|
||||
panel = node.addDOMWidget("xb_sb_preview", "preview", box, { serialize: false, hideOnZoom: false });
|
||||
} catch (err) {
|
||||
panel = null;
|
||||
console.warn("[XB-BOX 神笔] 显示窗不可用:", err);
|
||||
// 显示窗是 DOM 部件(addDOMWidget)。老版 ComfyUI 前端没有这个 API → 整个窗口都不会出现,
|
||||
// 节点上只剩几个按钮。这是「云机上完全看不到画布窗口」的第一嫌疑人,必须说出来。
|
||||
// 必需要求:ComfyUI 前端 ≥ 1.3(或任何带 addDOMWidget 的版本)。
|
||||
console.error("[XB-BOX 神笔] 显示窗不可用:当前 ComfyUI 前端不支持 DOM 部件(LGraphNode.addDOMWidget)。"
|
||||
+ "请把 ComfyUI / ComfyUI_frontend 升级到较新版本后刷新页面。", err);
|
||||
notify("显示窗不可用:当前 ComfyUI 前端太旧(缺 addDOMWidget),升级前端后刷新页面即可", "error");
|
||||
}
|
||||
if (panelImg) {
|
||||
// 存档 / 源图都是**服务端本机**的文件:工作流从别的机器(如本地 → 云机)拷过来、
|
||||
// 或云端 input 目录被重置时,这个文件不存在 → <img> 只会烂成一块"图裂"空窗
|
||||
// (用户看到的就是"画布显示不出来")。这里把它记进 badSrcs,refreshPanel 回落到
|
||||
// 空白画布 / 源图,并给出明确说明,而不是留一块黑框让人猜。
|
||||
panelImg.addEventListener("error", () => {
|
||||
const src = panelImg.getAttribute("src");
|
||||
if (!src || badSrcs.has(src)) return;
|
||||
// img 的 error 也可能是"上一次请求被中断"的迟到事件 → 用独立探针复核,避免误判好图
|
||||
const probe = new Image();
|
||||
probe.onerror = () => {
|
||||
if (badSrcs.size > 32) badSrcs.clear();
|
||||
badSrcs.add(src);
|
||||
reportBadSrc(src);
|
||||
refreshPanel();
|
||||
};
|
||||
probe.src = src;
|
||||
});
|
||||
}
|
||||
if (panel) {
|
||||
// 只给 computeLayoutSize(弹性)——不能给 computeSize,否则会被当成固定高度部件
|
||||
@@ -896,6 +952,34 @@ function setupNode(node) {
|
||||
try { panel.serialize = false; } catch (_) { /* ignore */ }
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示窗图片加载失败时的诊断输出。
|
||||
* 「云机上看不到画布」最常见的一条就是:工作流里存的是画板**文件名**,而那个文件只存在于
|
||||
* 另一台机器的 input 目录里 → /view 404 → <img> 什么都不显示(旧版本连提示都没有,只剩一块黑框)。
|
||||
*/
|
||||
const reportBadSrc = (src) => {
|
||||
const create = isCreateMode();
|
||||
const arch = create ? (paintValue(true) || transientPaint()) : paintValue(false);
|
||||
const label = (arch && String(paintingSrc(arch)) === String(src))
|
||||
? `画板存档「${archiveLabel(arch)}」`
|
||||
: `源图「${String(getVal(node, "image") || "")}」`;
|
||||
let file = "?";
|
||||
try {
|
||||
const u = new URL(src, location.href);
|
||||
const sub = u.searchParams.get("subfolder") || "";
|
||||
file = `input/${sub ? sub + "/" : ""}${u.searchParams.get("filename") || ""}`;
|
||||
} catch (_) { /* ignore */ }
|
||||
console.warn("[XB-BOX 神笔] ❌ 显示窗图片加载失败 → 已回落显示\n"
|
||||
+ ` · 类型:${label}\n`
|
||||
+ ` · URL:${src}\n`
|
||||
+ ` · 服务端文件:${file}\n`
|
||||
+ " · 排查:① 把上面的 URL 贴到浏览器地址栏:404 = 文件不在那台机器上,500 = 后端读不了\n"
|
||||
+ " ② 到该机器 ComfyUI 的 input 目录看这个文件在不在\n"
|
||||
+ " ③ 工作流是从别的机器(本地 ↔ 云机)拷来的?画板只记文件名,文件不会跟着走\n"
|
||||
+ " · 处理:在本机重新点「🎨 创作画布」/「🖌️ 编辑图片」作画并保存,即可重建存档");
|
||||
notify(`显示窗读不到${label}:该文件不在本机 input 目录,已回落显示(详情见浏览器控制台 [XB-BOX 神笔])`, "warn");
|
||||
};
|
||||
|
||||
function refreshPanel() {
|
||||
if (!panelImg) return;
|
||||
const create = isCreateMode();
|
||||
@@ -904,18 +988,39 @@ function setupNode(node) {
|
||||
const [targetW, targetH] = resolveTarget(node);
|
||||
const color = String(getVal(node, "canvas_color") || BG_WHITE);
|
||||
|
||||
// 画板存档:创作 = created_data(后端未重启时退到本次会话的临时存档);编辑 = painted_data
|
||||
const archive = create ? (painted || transientPaint()) : painted;
|
||||
const archiveUrl = archive ? paintingSrc(archive) : "";
|
||||
const sourceUrl = source ? viewUrl(source) : "";
|
||||
|
||||
// 创作模式没画过 → 纯色空白画布:直接用 CSS 背景铺色,**完全不经过 canvas**。
|
||||
// ⚠️ 原来这里是 blankCanvasUrl():建画布 → 填色 → toDataURL → <img>。
|
||||
// 但画布渲染失效时不会报错(画出来是透明的),于是"创作模式加载不出空白画布";
|
||||
// 而编辑模式走的是 <img src="/api/view?...">,不碰 canvas,所以一直正常。
|
||||
// ⚠️ 存档/源图在**服务端本机**读不到时(换机器、云端 input 被重置),后端是"忽略存档 →
|
||||
// 输出空白画布",前端同样回落,绝不把一块"图裂"空窗丢给用户。
|
||||
let url = "";
|
||||
let solid = null;
|
||||
if (painted) url = paintingSrc(painted);
|
||||
else if (create) {
|
||||
const temp = transientPaint();
|
||||
if (temp) url = paintingSrc(temp);
|
||||
else solid = color === BG_BLACK ? BLACK : WHITE;
|
||||
} else if (source) url = viewUrl(source);
|
||||
let note = "";
|
||||
if (archiveUrl) {
|
||||
if (badSrcs.has(archiveUrl)) {
|
||||
note = `⚠️ 画板存档读不到:${archiveLabel(archive)}\n(服务端 input/${PAINT_SUBFOLDER} 里没有这个文件)`;
|
||||
if (create) {
|
||||
solid = color === BG_BLACK ? BLACK : WHITE;
|
||||
note += `\n已按 ${targetW} × ${targetH} 显示空白画布;点「🎨 创作画布」重新作画即可`;
|
||||
} else if (sourceUrl && !badSrcs.has(sourceUrl)) {
|
||||
url = sourceUrl;
|
||||
note += "\n已回落到源图显示";
|
||||
}
|
||||
} else {
|
||||
url = archiveUrl;
|
||||
}
|
||||
} else if (create) {
|
||||
solid = color === BG_BLACK ? BLACK : WHITE;
|
||||
} else if (sourceUrl) {
|
||||
if (badSrcs.has(sourceUrl)) note = `⚠️ 源图读不到:${source}\n(服务端 input 目录里没有这个文件)`;
|
||||
else url = sourceUrl;
|
||||
}
|
||||
|
||||
if (panelBox) {
|
||||
panelBox.style.background = solid || "#141419";
|
||||
@@ -934,7 +1039,14 @@ function setupNode(node) {
|
||||
panelImg.removeAttribute("src");
|
||||
panelImg.style.display = "none";
|
||||
panelHint.style.display = "block";
|
||||
panelHint.textContent = "尚未作画\n请先在「图片」里选择或上传一张本地图片";
|
||||
panelHint.textContent = note || "尚未作画\n请先在「图片」里选择或上传一张本地图片";
|
||||
}
|
||||
|
||||
// 说明角标:有图 / 有纯色画布时用角标;都没有时上面那行提示文字已经说了,不用重复
|
||||
if (panelNote) {
|
||||
const badge = (url || solid) ? note : "";
|
||||
panelNote.style.display = badge ? "block" : "none";
|
||||
panelNote.textContent = badge;
|
||||
}
|
||||
|
||||
// 标注两个模式都显示:切换分辨率时这里会立刻反映目标尺寸
|
||||
|
||||
@@ -408,6 +408,9 @@
|
||||
"batch_size": {
|
||||
"name": "Batch Size"
|
||||
},
|
||||
"llm_enable": {
|
||||
"name": "LLM Enhance"
|
||||
},
|
||||
"clip_name": {
|
||||
"name": "Model (Load CLIP)"
|
||||
},
|
||||
|
||||
@@ -5315,6 +5315,9 @@
|
||||
"batch_size": {
|
||||
"name": "生成数量"
|
||||
},
|
||||
"llm_enable": {
|
||||
"name": "LLM 增强"
|
||||
},
|
||||
"clip_name": {
|
||||
"name": "模型·Load CLIP"
|
||||
},
|
||||
|
||||
+69
-46
@@ -10,7 +10,9 @@ XB-BOX - 🖼️ Qwen2.1提示词预设
|
||||
· 画幅比例 / 宽度 / 高度 / 生成数量
|
||||
· 元素面板提示词拼装(8 大类词表,前端 js/xb_qwen_prompt_preset.js)
|
||||
|
||||
② 文本生成(官方两个节点的参数,逐字对齐;没有开关,执行即生成)
|
||||
② 文本生成(官方两个节点的参数,逐字对齐)
|
||||
· 总开关 llm_enable(弹窗「🤖 LLM设置」顶部的单选「启用LLM增强 / 关闭LLM增强」,默认启用)
|
||||
⛔ 关闭 = 只输出拼装好的提示词 + 空latent:不加载文本编码器、不生成文本(模型与参数都不生效)
|
||||
· 模型选择 = 官方 Load CLIP:clip_name(models/text_encoders)+ type(默认 qwen_image)+ device(默认 default)
|
||||
· 参数设置 = 官方 Generate Text 全量参数:
|
||||
max_length / sampling_mode(on·off)/ temperature / top_k / top_p / min_p /
|
||||
@@ -711,7 +713,7 @@ _LANG_HINT = {
|
||||
}
|
||||
|
||||
_SETTING_KEYS = ("latent_kind", "output_lang", "preset_mode", "io_mode", "three_view_text", "aspect_ratio",
|
||||
"width", "height", "batch_size", "clip_name", "clip_type", "device",
|
||||
"width", "height", "batch_size", "llm_enable", "clip_name", "clip_type", "device",
|
||||
"max_length", "sampling_mode", "temperature", "top_k", "top_p", "min_p",
|
||||
"repetition_penalty", "presence_penalty", "seed", "thinking",
|
||||
"use_default_template", "mtp", "skill_name", "skill_mode", "internal_prompt")
|
||||
@@ -913,7 +915,15 @@ class XB_QwenPromptPreset:
|
||||
"tooltip": "一次生成的图片数量(空 latent 的 batch 维度)",
|
||||
}),
|
||||
|
||||
# ══ ② 官方 Load CLIP(模型选择)══
|
||||
# ══ ② LLM 增强总开关(弹窗「🤖 LLM设置」顶部的单选,默认启用)══
|
||||
"llm_enable": ("BOOLEAN", {
|
||||
"default": True,
|
||||
"label_on": "✅ 启用 LLM 增强", "label_off": "⛔ 关闭 LLM 增强",
|
||||
"tooltip": "启用(默认)= 用文本编码器把正文生成成完整提示词\n"
|
||||
"关闭 = 只输出拼装好的提示词 + 空latent(不加载文本编码器、不生成文本)",
|
||||
}),
|
||||
|
||||
# ══ ③ 官方 Load CLIP(模型选择)══
|
||||
"clip_name": (text_encoder_list(), {
|
||||
"tooltip": "文本编码器(models/text_encoders)。与官方 Load CLIP 同一个列表。",
|
||||
}),
|
||||
@@ -926,7 +936,7 @@ class XB_QwenPromptPreset:
|
||||
"tooltip": "设备:与官方 Load CLIP 一致;default=自动,cpu=放内存",
|
||||
}),
|
||||
|
||||
# ══ ③ 官方 Generate Text(参数设置,全量)══
|
||||
# ══ ④ 官方 Generate Text(参数设置,全量)══
|
||||
"max_length": ("INT", {
|
||||
"default": 512, "min": 1, "max": 32768,
|
||||
"tooltip": "Generate Text · max_length:最大生成长度",
|
||||
@@ -978,7 +988,7 @@ class XB_QwenPromptPreset:
|
||||
"tooltip": "Generate Text · mtp:多 token 预测投机解码;没有 MTP 权重时无效",
|
||||
}),
|
||||
|
||||
# ══ ④ 内部字段(节点表面由前端面板隐藏)══
|
||||
# ══ ⑤ 内部字段(节点表面由前端面板隐藏)══
|
||||
"internal_prompt": ("STRING", {
|
||||
"default": "", "multiline": True,
|
||||
"tooltip": "节点内拼装/编辑的提示词正文(随工作流保存)",
|
||||
@@ -1009,7 +1019,8 @@ class XB_QwenPromptPreset:
|
||||
FUNCTION = "generate"
|
||||
CATEGORY = "XB_ToolBox/Image_Params"
|
||||
DESCRIPTION = ("Qwen2.1提示词预设:生图预设(空latent类型/输出语言/预设模式/画幅参数 + 8 大类元素面板拼装)"
|
||||
"+ 官方 Load CLIP 与官方 Generate Text(纯本地文本生成,无开关,执行即生成)。"
|
||||
"+ 官方 Load CLIP 与官方 Generate Text(纯本地文本生成)。"
|
||||
"「启用 / 关闭 LLM 增强」在弹窗「🤖 LLM设置」顶部(默认启用;关闭 = 只输出拼装好的提示词)。"
|
||||
"输出:提示词 / 空latent。")
|
||||
|
||||
@classmethod
|
||||
@@ -1024,6 +1035,7 @@ class XB_QwenPromptPreset:
|
||||
clip_name, clip_type, device,
|
||||
max_length, sampling_mode, temperature, top_k, top_p, min_p,
|
||||
repetition_penalty, presence_penalty, seed, thinking, use_default_template, mtp,
|
||||
llm_enable=True, # LLM 增强总开关(默认启用;关闭 = 不加载文本编码器、不生成文本)
|
||||
internal_prompt="", manager_settings="", text="", images=None, unique_id=None,
|
||||
skill_name=SKILL_NONE, skill_mode=SKILL_MODE_AUTO, io_mode=IO_MODE_AUTO):
|
||||
# ① 画幅归一 + 空 latent(与「🖼️ 生图提示词预设」逐行一致)
|
||||
@@ -1054,7 +1066,6 @@ class XB_QwenPromptPreset:
|
||||
with_lang_hint=not bool(skill_text))
|
||||
|
||||
# ③ 官方 Load CLIP + 官方 Generate Text(参数与调用方式逐字对齐官方节点)
|
||||
clip = get_clip(clip_name, clip_type, device)
|
||||
# 官方 image 输入:一批图 = N 张参考图(官方图像编辑最多 10 张,提示词可引用「第 N 张图」)
|
||||
# ⚠️ 不能把多帧当 video:Qwen-Image-2.1 的文本编码器链里**没人读 video**
|
||||
# (qwen_image21/qwen3vl/sd1_clip 全程不碰 video,全库只有 gemma4 那个 tokenizer 认),
|
||||
@@ -1069,55 +1080,67 @@ class XB_QwenPromptPreset:
|
||||
if mode_needs_image(preset_mode) and img_count == 0:
|
||||
safe_print(f"[XB-Qwen提示词预设] ⚠️ 当前档位「{preset_mode}」属于图生图,但没有接参考图"
|
||||
f"(🖼️ 图像,多张按批接即可)→ 本次按「文生图」设定词纯文字生成")
|
||||
try: # 日志留痕:看得见“到底送了什么给文本编码器”
|
||||
print(f"[XB-Qwen提示词预设] 送模型 prompt({len(model_prompt)} 字):"
|
||||
f"{model_prompt[:120].replace(chr(10), ' / ')}" + ("…" if len(model_prompt) > 120 else ""))
|
||||
except Exception:
|
||||
pass
|
||||
# SKILL 已在上方读入 skill_text,并在那里决定了要不要叠加语言提示
|
||||
tokens = clip.tokenize(
|
||||
model_prompt,
|
||||
image=image_arg,
|
||||
skip_template=not bool(use_default_template),
|
||||
min_length=1,
|
||||
thinking=bool(thinking),
|
||||
system_prompt=skill_text,
|
||||
)
|
||||
_mtp = False if mtp == "off" else (True if mtp == "auto" else int(mtp))
|
||||
generated_ids = clip.generate(
|
||||
tokens,
|
||||
do_sample=(sampling_mode == "on"),
|
||||
max_length=int(max_length),
|
||||
temperature=float(temperature),
|
||||
top_k=int(top_k),
|
||||
top_p=float(top_p),
|
||||
min_p=float(min_p),
|
||||
repetition_penalty=float(repetition_penalty),
|
||||
presence_penalty=float(presence_penalty),
|
||||
seed=int(seed),
|
||||
mtp=_mtp,
|
||||
)
|
||||
raw = clip.decode(generated_ids) or ""
|
||||
|
||||
# 思考段分离(与官方 TextGenerate 完全一致:不闭合时整段都算正文)
|
||||
reasoning, _, gen_text = raw.partition("</think>")
|
||||
if not reasoning.lstrip().startswith("<think>") and not model_prompt.rstrip().endswith("<think>"):
|
||||
gen_text = raw
|
||||
gen_text = gen_text.strip()
|
||||
# ⛔「关闭 LLM 增强」(默认启用):整段跳过 —— 不加载文本编码器、不生成文本
|
||||
gen_text = ""
|
||||
if llm_enable:
|
||||
clip = get_clip(clip_name, clip_type, device)
|
||||
try: # 日志留痕:看得见“到底送了什么给文本编码器”
|
||||
print(f"[XB-Qwen提示词预设] 送模型 prompt({len(model_prompt)} 字):"
|
||||
f"{model_prompt[:120].replace(chr(10), ' / ')}" + ("…" if len(model_prompt) > 120 else ""))
|
||||
except Exception:
|
||||
pass
|
||||
# SKILL 已在上方读入 skill_text,并在那里决定了要不要叠加语言提示
|
||||
tokens = clip.tokenize(
|
||||
model_prompt,
|
||||
image=image_arg,
|
||||
skip_template=not bool(use_default_template),
|
||||
min_length=1,
|
||||
thinking=bool(thinking),
|
||||
system_prompt=skill_text,
|
||||
)
|
||||
_mtp = False if mtp == "off" else (True if mtp == "auto" else int(mtp))
|
||||
generated_ids = clip.generate(
|
||||
tokens,
|
||||
do_sample=(sampling_mode == "on"),
|
||||
max_length=int(max_length),
|
||||
temperature=float(temperature),
|
||||
top_k=int(top_k),
|
||||
top_p=float(top_p),
|
||||
min_p=float(min_p),
|
||||
repetition_penalty=float(repetition_penalty),
|
||||
presence_penalty=float(presence_penalty),
|
||||
seed=int(seed),
|
||||
mtp=_mtp,
|
||||
)
|
||||
raw = clip.decode(generated_ids) or ""
|
||||
|
||||
# 思考段分离(与官方 TextGenerate 完全一致:不闭合时整段都算正文)
|
||||
reasoning, _, gen_text = raw.partition("</think>")
|
||||
if not reasoning.lstrip().startswith("<think>") and not model_prompt.rstrip().endswith("<think>"):
|
||||
gen_text = raw
|
||||
gen_text = gen_text.strip()
|
||||
else:
|
||||
safe_print("[XB-Qwen提示词预设] ⛔ 已关闭 LLM 增强 → 只输出拼装好的提示词"
|
||||
"(不加载文本编码器、不生成文本)")
|
||||
|
||||
# ④ 设定词原封不动置顶(与未生成时的成句规则一致;t2i/i2i 模版条款同样进最终提示词)
|
||||
final_prompt = build_prompt(lang, preset_mode, three_view_text, gen_text,
|
||||
# 关闭 LLM 增强时正文用「外接提示词优先」的那一份(与外接端口时的成句规则一致)
|
||||
final_prompt = build_prompt(lang, preset_mode, three_view_text,
|
||||
gen_text if llm_enable else body,
|
||||
has_image=has_img, io_mode=io_mode)
|
||||
|
||||
try:
|
||||
shape = tuple(latent.get("samples").shape)
|
||||
except Exception:
|
||||
shape = ()
|
||||
llm_state = (f"丨CLIP {clip_name}({clip_type}·{device})"
|
||||
f"丨Text 最大长度 {max_length}·采样 {sampling_mode}·思考 {'开' if thinking else '关'}"
|
||||
f"丨SKILL {skill_used}({len(skill_text)} 字·{skill_mode_of(skill_mode)})"
|
||||
) if llm_enable else "丨LLM 增强 关闭(不加载 CLIP、不生成文本)"
|
||||
print(f"[XB-Qwen提示词预设] {spec['label']} 丨 {w}x{h}({aspect_ratio}·步长 {spec['step']})"
|
||||
f"丨数量 {shape[0] if shape else batch_size}丨空latent {shape}"
|
||||
f"丨CLIP {clip_name}({clip_type}·{device})"
|
||||
f"丨Text 最大长度 {max_length}·采样 {sampling_mode}·思考 {'开' if thinking else '关'}"
|
||||
f"丨SKILL {skill_used}({len(skill_text)} 字·{skill_mode_of(skill_mode)})"
|
||||
f"{llm_state}"
|
||||
f"丨模式 {preset_mode}丨模版 {io_eff}"
|
||||
f"{'(自动' + (':检测到参考图' if has_img else ':未接参考图') + ')' if io_mode_of(io_mode) == IO_MODE_AUTO else ''}"
|
||||
f"丨参考图 {img_count} 张"
|
||||
@@ -1125,7 +1148,7 @@ class XB_QwenPromptPreset:
|
||||
|
||||
ui = {
|
||||
"text": [final_prompt],
|
||||
"model": [str(clip_name or "-")],
|
||||
"model": [str(clip_name or "-") if llm_enable else "- (已关闭 LLM 增强)"],
|
||||
"gen_len": [len(gen_text)],
|
||||
}
|
||||
# 种子回写不在后端做:节点上的 seed widget 由 ComfyUI 原生的
|
||||
|
||||
Reference in New Issue
Block a user