diff --git a/__init__.py b/__init__.py index bdc4785..ebb72cb 100644 --- a/__init__.py +++ b/__init__.py @@ -1,5 +1,8 @@ import os import traceback + +__version__ = "0.05.31" + from server import PromptServer from aiohttp import web @@ -58,6 +61,7 @@ try: XB_WanAnimateToVideo) # --- ROCm 节点:8个自包含节点 --- + from .nodes_label import XB_CanvasLabel from .nodes_segmentation import XB_HumanSegModelLoader, XB_HumanSegmentation from .nodes_rocm import (XB_ROCmKSampler, XB_ROCmKSamplerAdvanced, XB_ROCmSamplerCustom, XB_ROCmSamplerCustomAdvanced, @@ -110,7 +114,8 @@ try: "XB_WanAnimate_ParamBus": XB_WanAnimate_ParamBus, "XB_WanAnimate_RelayNode": XB_WanAnimate_RelayNode, "XB_HumanSegmentation": XB_HumanSegmentation, - "XB_HumanSegModelLoader": XB_HumanSegModelLoader + "XB_HumanSegModelLoader": XB_HumanSegModelLoader, + "XB_CanvasLabel": XB_CanvasLabel } NODE_DISPLAY_NAME_MAPPINGS = { @@ -156,11 +161,10 @@ try: "XB_WanAnimate_ParamBus": "XB-BOX - 🎬 Animate 动作迁移总线", "XB_WanAnimate_RelayNode": "XB-BOX - 🏃‍♀️ Animate 无限接力点", "XB_HumanSegmentation": "XB-BOX - ✂️ 人物分割 (DirectML/ROCm)", - "XB_HumanSegModelLoader": "XB-BOX - 📥 人物分割模型加载" + "XB_HumanSegModelLoader": "XB-BOX - 📥 人物分割模型加载", + "XB_CanvasLabel": "XB-BOX - 🏷️ Canvas Label (文字标签)" } - print_success(" 🚀 ROCm: KSampler | KSamplerAdvanced | SamplerCustom | SamplerCustomAdvanced | VAE Decode | VAE Encode | VAE Decode Temporal | MemCleaner") - print_success("\n" + "="*50) print_success("🚀 [XB-BOX] XB_ToolBox Core Modules Loaded Successfully!") print_success("="*50 + "\n") diff --git a/js/xb_label.js b/js/xb_label.js new file mode 100644 index 0000000..482df8e --- /dev/null +++ b/js/xb_label.js @@ -0,0 +1,181 @@ +import { app } from "../../scripts/app.js"; + +// ============================================================ +// XB_CanvasLabel — 文字标签节点 +// ============================================================ + +// ---- 1. 劫持 drawNode(隐藏默认边框/标题)---- +const oldDrawNode = LGraphCanvas.prototype.drawNode; +LGraphCanvas.prototype.drawNode = function (node, ctx) { + if (node.comfyClass !== "XB_CanvasLabel" && node.type !== "XB_CanvasLabel") + return oldDrawNode.apply(this, arguments); + + node.bgcolor = node.color = "transparent"; + const savedTitle = node.title, savedCtor = node.constructor.title; + node.title = node.constructor.title = ""; + const result = oldDrawNode.apply(this, arguments); + node.title = savedTitle; + node.constructor.title = savedCtor; + if (node.draw) node.draw(ctx); + return result; +}; + +// ---- 2. 注册扩展 ---- +app.registerExtension({ + name: "XB_ToolBox.CanvasLabel", + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData.name !== "XB_CanvasLabel") return; + + nodeData.title_mode = LiteGraph.NO_TITLE; + nodeData.collapsable = false; + + // 属性面板定义(颜色不设 @ → 不会在画布上生成输入框) + nodeData["@fontSize"] = { type: "number", default: 40 }; + nodeData["@textAlign"] = { type: "combo", values: ["left", "center", "right"], default: "left" }; + nodeData["@padding"] = { type: "number", default: 0 }; + nodeData["@borderRadius"] = { type: "number", default: 0 }; + nodeData["@angle"] = { type: "number", default: 0 }; + + // ---- onNodeCreated ---- + const origOnNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + origOnNodeCreated?.apply(this, arguments); + + const p = this.properties = this.properties || {}; + p.fontSize = p.fontSize ?? 40; + p.fontColor = p.fontColor ?? "#ffffff"; + p.textAlign = p.textAlign ?? "left"; + p.backgroundColor = p.backgroundColor ?? "transparent"; + p.padding = p.padding ?? 0; + p.borderRadius = p.borderRadius ?? 0; + p.angle = p.angle ?? 0; + + this.title = this.title || "机智罗"; + this.size = [320, 80]; + }; + + // ---- draw():自定义绘制 ---- + nodeType.prototype.draw = function (ctx) { + if (!this.flags) this.flags = {}; + this.flags.allow_interaction = !this.flags.pinned; + ctx.save(); + + const p = this.properties || {}; + const fontSize = Math.max(1, Math.min(800, +p.fontSize || 40)); + const fontColor = p.fontColor || "#ffffff"; + const textAlign = p.textAlign || "left"; + const bgColor = p.backgroundColor || ""; + const pad = +p.padding || 0; + const borderRadius = +p.borderRadius || 0; + const angleDeg = parseInt(p.angle) || 0; + + ctx.font = fontSize + "px Arial"; + + const raw = (this.title || "").replace(/\\n/g, "\n").replace(/\n*$/, ""); + const lines = raw.length === 0 ? [""] : raw.split("\n"); + + let maxW = 1; + lines.forEach(s => { const w = ctx.measureText(s).width; if (w > maxW) maxW = w; }); + this.size[0] = maxW + pad * 2; + this.size[1] = fontSize * lines.length + pad * 2; + + if (angleDeg) { + const cx = this.size[0] / 2, cy = this.size[1] / 2; + ctx.translate(cx, cy); + ctx.rotate((angleDeg * Math.PI) / 180); + ctx.translate(-cx, -cy); + } + + if (bgColor && bgColor !== "transparent") { + ctx.fillStyle = bgColor; + ctx.beginPath(); + if (ctx.roundRect) { + ctx.roundRect(0, 0, this.size[0], this.size[1], [borderRadius]); + } else if (borderRadius > 0) { + const r = borderRadius, w = this.size[0], h = this.size[1]; + ctx.moveTo(r, 0); ctx.lineTo(w - r, 0); + ctx.quadraticCurveTo(w, 0, w, r); ctx.lineTo(w, h - r); + ctx.quadraticCurveTo(w, h, w - r, h); ctx.lineTo(r, h); + ctx.quadraticCurveTo(0, h, 0, h - r); ctx.lineTo(0, r); + ctx.quadraticCurveTo(0, 0, r, 0); + } else { + ctx.rect(0, 0, this.size[0], this.size[1]); + } + ctx.fill(); + } + + ctx.fillStyle = fontColor; + ctx.textBaseline = "top"; + ctx.textAlign = textAlign; + + let tx = textAlign === "center" ? this.size[0] / 2 : textAlign === "right" ? this.size[0] - pad : pad; + lines.forEach((line, i) => ctx.fillText(line || " ", tx, pad + i * fontSize)); + + ctx.restore(); + }; + + // ---- 双击 → 属性面板 ---- + const origDblClick = nodeType.prototype.onDblClick; + nodeType.prototype.onDblClick = function (e, pos, gc) { + if (origDblClick) { const r = origDblClick.apply(this, arguments); if (r !== undefined && r !== false) return r; } + LGraphCanvas.active_canvas.showShowNodePanel(this); + return true; + }; + + // ---- 右键菜单 ---- + const origMenu = nodeType.prototype.getExtraMenuOptions; + nodeType.prototype.getExtraMenuOptions = function (_, options) { + origMenu?.apply(this, arguments); + const self = this; + options.unshift( + { content: "✏️ 编辑文字", callback: () => { + const old = self.title || ""; + const val = prompt("输入文字(\\n 换行):", old); + if (val !== null && val !== old) { + self.title = val || "机智罗"; + self.setDirtyCanvas(true, true); + app.graph.setDirtyCanvas(true, false); + } + }}, + { content: "🎨 字体颜色…", callback: () => { + const inp = document.createElement("input"); + inp.type = "color"; + inp.value = self.properties.fontColor || "#ffffff"; + inp.style.cssText = "position:fixed;top:50%;left:50%;width:1px;height:1px;opacity:0.01;z-index:99999;"; + document.body.appendChild(inp); + inp.oninput = function () { + self.properties.fontColor = this.value; + self.setDirtyCanvas(true, true); + app.graph.setDirtyCanvas(true, false); + }; + inp.onchange = function () { + self.properties.fontColor = this.value; + self.setDirtyCanvas(true, true); + app.graph.setDirtyCanvas(true, false); + if (document.body.contains(this)) document.body.removeChild(this); + }; + requestAnimationFrame(() => inp.click()); + }}, + { content: "🖌️ 背景颜色…", callback: () => { + const inp = document.createElement("input"); + inp.type = "color"; + inp.value = self.properties.backgroundColor === "transparent" ? "#000000" : (self.properties.backgroundColor || "#000000"); + inp.style.cssText = "position:fixed;top:50%;left:50%;width:1px;height:1px;opacity:0.01;z-index:99999;"; + document.body.appendChild(inp); + inp.oninput = function () { + self.properties.backgroundColor = this.value; + self.setDirtyCanvas(true, true); + app.graph.setDirtyCanvas(true, false); + }; + inp.onchange = function () { + self.properties.backgroundColor = this.value; + self.setDirtyCanvas(true, true); + app.graph.setDirtyCanvas(true, false); + if (document.body.contains(this)) document.body.removeChild(this); + }; + requestAnimationFrame(() => inp.click()); + }} + ); + }; + }, +}); diff --git a/nodes_blockswap.py b/nodes_blockswap.py index 8760a8e..7da8c7b 100644 --- a/nodes_blockswap.py +++ b/nodes_blockswap.py @@ -16,8 +16,12 @@ class AnyType(str): return False anyType = AnyType("*") -def is_dynamic_vram_flag_used(): - return "--enable-dynamic-vram" in sys.argv +def is_dynamic_vram_active(): + """仅当用户显式传参 --enable-dynamic-vram 时视为动态显存激活,分块节点休眠。""" + if "--enable-dynamic-vram" in sys.argv: + print("\n\033[93m[XB Block Swap]\033[0m: ⚠️ 检测到 --enable-dynamic-vram,动态显存已启用。分块节点自动休眠。") + return True + return False # ============================================================================== # 🛡️ 核心防爆雷达:判定是否为不可分块的异常/强耦合模型 @@ -56,8 +60,7 @@ class XB_UNetBlockSwap: if not isinstance(unet_model, ModelPatcher): return (unet_model,) - if is_dynamic_vram_flag_used(): - print("\n\033[92m[XB UNet Block Swap]\033[0m: ⚠️ Detected parameter [--enable-dynamic-vram], block swap node auto-sleeping.") + if is_dynamic_vram_active(): return (unet_model,) def swap_blocks(model_patcher: ModelPatcher, device_to, lowvram_model_memory, force_patch_weights, full_load): @@ -93,7 +96,7 @@ class XB_UNetBlockSwap: if not all_blocks: return - print(f"\033[96m[XB UNet Block Swap]\033[0m: Static physical block swap activated! Locked {len(all_blocks)} engine modules.") + print(f"\033[96m[XB UNet Block Swap]\033[0m: 静态物理分块交换已激活!已锁定 {len(all_blocks)} 个引擎模块。") for b, block in tqdm(enumerate(all_blocks), total=len(all_blocks), desc="Slicing UNet pipeline"): if b > blocks_to_swap: @@ -131,8 +134,7 @@ class XB_CheckpointBlockSwap: if not isinstance(checkpoint_model, ModelPatcher): return (checkpoint_model,) - if is_dynamic_vram_flag_used(): - print("\n\033[92m[XB Checkpoint Block Swap]\033[0m: ⚠️ Detected parameter [--enable-dynamic-vram], block swap node auto-sleeping.") + if is_dynamic_vram_active(): return (checkpoint_model,) def swap_blocks(model_patcher: ModelPatcher, device_to, lowvram_model_memory, force_patch_weights, full_load): @@ -166,12 +168,12 @@ class XB_CheckpointBlockSwap: all_blocks.append(attr) if all_blocks: - print(f"\033[96m[XB Checkpoint Block Swap]\033[0m: Static physical block swap activated! Locked {len(all_blocks)} engine modules.") + print(f"\033[96m[XB Checkpoint Block Swap]\033[0m: 静态物理分块交换已激活!已锁定 {len(all_blocks)} 个引擎模块。") for b, block in tqdm(enumerate(all_blocks), total=len(all_blocks), desc="Slicing Checkpoint pipeline"): if b > blocks_to_swap: block.to(main_device) else: - block.to(model_patcher.offload_device) + block.to(model_patcher.offload_device) if offload_txt_emb: for path in ['text_embedding', 'caption_encoder', 'text_encoder']: diff --git a/nodes_label.py b/nodes_label.py new file mode 100644 index 0000000..f94fb45 --- /dev/null +++ b/nodes_label.py @@ -0,0 +1,21 @@ +""" +XB-BOX Canvas Label — 画布文字标签节点 +====================================== +纯展示型节点,用于在工作流画布上添加说明文字。 +所有渲染逻辑由前端 JS (xb_label.js) 处理。 +""" + + +class XB_CanvasLabel: + """纯显示标签节点 - 在画布上展示自定义文字,兼容所有 ComfyUI 版本""" + + @classmethod + def INPUT_TYPES(cls): + return {"required": {}} + + RETURN_TYPES = () + FUNCTION = "noop" + CATEGORY = "XB_ToolBox/Display" + + def noop(self, **kwargs): + return ()