From 62f1c8dac471505032a375f0d8a57014c962bc7f Mon Sep 17 00:00:00 2001 From: wjluoxiao <331030818@qq.com> Date: Fri, 15 May 2026 21:05:45 +0800 Subject: [PATCH] Add files via upload --- README.md | 146 +++++++++--------- __init__.py | 58 ++++--- js/folder_picker.js | 16 +- js/xb_dashboard.js | 62 +++----- js/xb_master_param.js | 28 +--- js/xb_smart_ui.js | 14 +- js/xb_video.js | 21 +-- js/xb_wiring.js | 27 ++-- locales/zh/main.json | 49 ++++++ locales/zh/nodeDefs.json | 321 +++++++++++++++++++++++++++++++++++++++ nodes_batch.py | 23 +-- nodes_blockswap.py | 38 ++--- nodes_dashboard.py | 3 +- nodes_pipeline.py | 108 ++++++------- nodes_sageatt.py | 81 ++++++++++ nodes_tile.py | 13 +- nodes_video.py | 40 +++-- nodes_vis.py | 154 ++++++++----------- nodes_vram.py | 66 ++++---- nodes_wan_vae.py | 80 ++++------ nodes_wiring.py | 29 +--- 21 files changed, 806 insertions(+), 571 deletions(-) create mode 100644 locales/zh/main.json create mode 100644 locales/zh/nodeDefs.json create mode 100644 nodes_sageatt.py diff --git a/README.md b/README.md index 5d2d728..5e63c61 100644 --- a/README.md +++ b/README.md @@ -1,74 +1,74 @@ -
- 🌐 English | 简体中文 -
-
- -# 🧰 ComfyUI XB-BOX (XB_ToolBox) - ---- - -## 🌟 Core Philosophy - -The primary goal of **XB_ToolBox** is to help AI beginners new to ComfyUI quickly master workflows, making local deployment and execution simpler and more convenient. It is a comprehensive ComfyUI extension suite encompassing both front-end interaction and low-level memory scheduling. - -- **Unified Parameters:** Integrates frequently used but scattered parameter nodes, allowing you to set essential image and video parameters from a single hub. -- **Visual Learning:** Provides visual chunking and parameter preview nodes, enabling beginners to intuitively understand the concepts of VAE/Encoder chunks, temporal slicing, and overlap effects. -- **UX Enhancements:** Introduces a user-friendly "Mirror Clone Console" (Dashboard) that clones major operation nodes from complex workflows into a unified, clean, and highly efficient control panel. -- **Extreme VRAM Optimization:** Through "Spatiotemporal Chunk Hijacking", "Dynamic VRAM Offloading for UNet/Checkpoints", and "Deep LiteGraph Customization", it allows consumer-grade GPUs to smoothly run massive 14B~22B 3D-DiT video models that would normally cause instant OOM (Out of Memory). - ---- - -![界面截图](docs/01.png) -![界面截图](docs/02.png) -![界面截图](docs/03.png) -![界面截图](docs/04.png) - -![界面截图](docs/05.png) -![界面截图](docs/06.png) -![界面截图](docs/04.png) - ---- - -### ✨ Core Nodes - -#### 1. 🎬 Media Parameters & Spatiotemporal Visualization - -- **🖼️ Image Parameters Master**: Controls core image generation parameters (Width, Height, Batch Size, Strength) in one place. Enforces safe steps to prevent invalid inputs and automatically aligns dimensions when a fixed aspect ratio is selected. -- **🎬 Video Parameters Master**: Manages Width, Height, Frames, and FPS (Integer/Float). Features a geek-level "Auto-Manual Transmission Engine" that smoothly snaps to official pre-trained "Golden Buckets" (e.g., 480x832, 544x960) and strictly locks the physical frame count to the safe `1+8N` rule. Dynamically displays video duration based on frame rate. -- **🧊 Spatiotemporal Chunk Visualization**: An exclusive dual-zone radar! The 2D left panel visualizes spatial image chunks and overlap values, while the 3D right panel uses a painter's algorithm to render temporal cylinder stacks. Saves beginners from the exhaustion of blind parameter tuning. -- 🧊 **Storyboard Image Slicer:** Offers multiple modes to slice common 4-grid, 6-grid, 9-grid... images all at once and output multiple individual images. This makes image-to-video or start/end frame video generation much more convenient, and is highly effective for creating short dramas. -- **📟 VRAM Calculator & Data Radar**: Predicts VRAM footprint for WAN/LTX models under different quantizations (FP8/GGUF) and weighs tensor volumes with MB-level precision. Significantly reduces trial-and-error costs by helping users choose models fitting their VRAM capacity. - -#### 2. 🧊 VRAM Optimization - -- **🧊 Sampler Chunk Master**: Tailored for WAN/LTX. Performs spatiotemporal dual-axis slicing (Spatial Tiles & Temporal Chunks) in the Latent space. Hijacks model parameters in-place with an built-in `rocm_optimized` extreme memory reclamation strategy, breaking the large-tensor OOM curse. -- **✂️ Model Block Swap**: Supports both UNet and Checkpoint modes. Forcibly unloads the first N Transformer Blocks and Text/Image embedding layers to system RAM, trading time for space. -- **🧹 VRAM Cleaner**: Essential for pre-generation clearing. Delivers "nuclear-level cleanup" by deeply invoking `gc.collect()` and `torch.cuda.empty_cache()` to crush PyTorch cache fragmentation. - -#### 3. 🎛️ Workflow Deployment Assistants - -- **🪄 XB Dashboard Zen**: A "Mirror Clone Console" developed using low-level LiteGraph features. Batch-clones any scattered components in your workflow into a fixed panel. Features two-way data synchronization and direct arterial connections to create a clean, customized monitoring dashboard. -- **🎛️ Dynamic Bus**: An ultra-compact N-in/N-out `AnyType` universal bus node. Provides custom UI type labels and one-click channel addition/removal to save your workflow from "noodle wire" chaos. - ---- - -### 📦 Installation - -**Method 1: ComfyUI Manager (Recommended)** -Search for `XB_ToolBox` in the ComfyUI Manager and click install. - -**Method 2: Manual Git Clone** -Navigate to your ComfyUI `custom_nodes` directory and run: - -```bash -git clone https://github.com/wjluoxiao/XB_ToolBox.git -``` - -(Note: This extension relies purely on the native ComfyUI ecosystem through elegant Python/JS architecture. NO extra pip dependencies required! Plug and play.) - ---- - -📝 License -This project is open-sourced under the Apache-2.0 License. It guarantees the freedom to share while protecting the patent defense rights of the core architecture code. - +
+ 🌐 English | 简体中文 +
+
+ +# 🧰 ComfyUI XB-BOX (XB_ToolBox) + +--- + +## 🌟 Core Philosophy + +The primary goal of **XB_ToolBox** is to help AI beginners new to ComfyUI quickly master workflows, making local deployment and execution simpler and more convenient. It is a comprehensive ComfyUI extension suite encompassing both front-end interaction and low-level memory scheduling. + +- **Unified Parameters:** Integrates frequently used but scattered parameter nodes, allowing you to set essential image and video parameters from a single hub. +- **Visual Learning:** Provides visual chunking and parameter preview nodes, enabling beginners to intuitively understand the concepts of VAE/Encoder chunks, temporal slicing, and overlap effects. +- **UX Enhancements:** Introduces a user-friendly "Mirror Clone Console" (Dashboard) that clones major operation nodes from complex workflows into a unified, clean, and highly efficient control panel. +- **Extreme VRAM Optimization:** Through "Spatiotemporal Chunk Hijacking", "Dynamic VRAM Offloading for UNet/Checkpoints", and "Deep LiteGraph Customization", it allows consumer-grade GPUs to smoothly run massive 14B~22B 3D-DiT video models that would normally cause instant OOM (Out of Memory). + +--- + +![界面截图](docs/01.png) +![界面截图](docs/02.png) +![界面截图](docs/03.png) +![界面截图](docs/04.png) + +![界面截图](docs/05.png) +![界面截图](docs/06.png) +![界面截图](docs/04.png) + +--- + +### ✨ Core Nodes + +#### 1. 🎬 Media Parameters & Spatiotemporal Visualization + +- **🖼️ Image Parameters Master**: Controls core image generation parameters (Width, Height, Batch Size, Strength) in one place. Enforces safe steps to prevent invalid inputs and automatically aligns dimensions when a fixed aspect ratio is selected. +- **🎬 Video Parameters Master**: Manages Width, Height, Frames, and FPS (Integer/Float). Features a geek-level "Auto-Manual Transmission Engine" that smoothly snaps to official pre-trained "Golden Buckets" (e.g., 480x832, 544x960) and strictly locks the physical frame count to the safe `1+8N` rule. Dynamically displays video duration based on frame rate. +- **🧊 Spatiotemporal Chunk Visualization**: An exclusive dual-zone radar! The 2D left panel visualizes spatial image chunks and overlap values, while the 3D right panel uses a painter's algorithm to render temporal cylinder stacks. Saves beginners from the exhaustion of blind parameter tuning. +- 🧊 **Storyboard Image Slicer:** Offers multiple modes to slice common 4-grid, 6-grid, 9-grid... images all at once and output multiple individual images. This makes image-to-video or start/end frame video generation much more convenient, and is highly effective for creating short dramas. +- **📟 VRAM Calculator & Data Radar**: Predicts VRAM footprint for WAN/LTX models under different quantizations (FP8/GGUF) and weighs tensor volumes with MB-level precision. Significantly reduces trial-and-error costs by helping users choose models fitting their VRAM capacity. + +#### 2. 🧊 VRAM Optimization + +- **🧊 Sampler Chunk Master**: Tailored for WAN/LTX. Performs spatiotemporal dual-axis slicing (Spatial Tiles & Temporal Chunks) in the Latent space. Hijacks model parameters in-place with an built-in `rocm_optimized` extreme memory reclamation strategy, breaking the large-tensor OOM curse. +- **✂️ Model Block Swap**: Supports both UNet and Checkpoint modes. Forcibly unloads the first N Transformer Blocks and Text/Image embedding layers to system RAM, trading time for space. +- **🧹 VRAM Cleaner**: Essential for pre-generation clearing. Delivers "nuclear-level cleanup" by deeply invoking `gc.collect()` and `torch.cuda.empty_cache()` to crush PyTorch cache fragmentation. + +#### 3. 🎛️ Workflow Deployment Assistants + +- **🪄 XB Dashboard Zen**: A "Mirror Clone Console" developed using low-level LiteGraph features. Batch-clones any scattered components in your workflow into a fixed panel. Features two-way data synchronization and direct arterial connections to create a clean, customized monitoring dashboard. +- **🎛️ Dynamic Bus**: An ultra-compact N-in/N-out `AnyType` universal bus node. Provides custom UI type labels and one-click channel addition/removal to save your workflow from "noodle wire" chaos. + +--- + +### 📦 Installation + +**Method 1: ComfyUI Manager (Recommended)** +Search for `XB_ToolBox` in the ComfyUI Manager and click install. + +**Method 2: Manual Git Clone** +Navigate to your ComfyUI `custom_nodes` directory and run: + +```bash +git clone https://github.com/wjluoxiao/XB_ToolBox.git +``` + +(Note: This extension relies purely on the native ComfyUI ecosystem through elegant Python/JS architecture. NO extra pip dependencies required! Plug and play.) + +--- + +📝 License +This project is open-sourced under the Apache-2.0 License. It guarantees the freedom to share while protecting the patent defense rights of the core architecture code. + --- \ No newline at end of file diff --git a/__init__.py b/__init__.py index 1bfb12c..1c84e37 100644 --- a/__init__.py +++ b/__init__.py @@ -6,13 +6,11 @@ from aiohttp import web @PromptServer.instance.routes.post("/xb_toolbox/choose_folder") async def choose_folder(request): - root = tk.Tk() root.withdraw() root.attributes('-topmost', True) folder_path = filedialog.askdirectory() root.destroy() - return web.json_response({"path": folder_path}) def print_success(msg): @@ -29,7 +27,6 @@ NODE_DISPLAY_NAME_MAPPINGS = {} WEB_DIRECTORY = "./js" try: - from .nodes_vis import XB_VRAM_Calculator, XB_ChunkVisualization from .nodes_vram import XTX_VRAM_Cleaner, XTX_Data_Radar from .nodes_video import XB_VideoParamsMaster, XB_ImageParamsMaster, XB_MasterParameter @@ -40,6 +37,7 @@ try: from .nodes_wan_vae import XB_WanImageToVideo, XB_WanFirstLastFrameToVideo from .nodes_batch import XB_BatchFolderLoader from .nodes_pipeline import XB_Wan_ParamBus, XB_Wan_RelayNode, XB_Video_Merger, XB_StoryboardSlicer + from .nodes_sageatt import XB_SageAttentionAccelerator NODE_CLASS_MAPPINGS = { "XB_VRAM_Calculator": XB_VRAM_Calculator, @@ -62,43 +60,43 @@ try: "XB_Wan_ParamBus": XB_Wan_ParamBus, "XB_Wan_RelayNode": XB_Wan_RelayNode, "XB_Video_Merger": XB_Video_Merger, - "XB_StoryboardSlicer": XB_StoryboardSlicer + "XB_StoryboardSlicer": XB_StoryboardSlicer, + "XB_SageAttentionAccelerator": XB_SageAttentionAccelerator } NODE_DISPLAY_NAME_MAPPINGS = { - "XB_VRAM_Calculator": "XB-BOX - 📟 可用显存计算", - "XB_ChunkVisualization": "XB-BOX - 🧊 时空分块预览", - "XTX_VRAM_Cleaner": "XB-BOX- 🧹 显存清理大师", - "XTX_Data_Radar": "XB-BOX - 🪞 生成数据预览", - "XB_VideoParamsMaster": "XB-BOX - 🎬 视频参数大全", - "XB_ImageParamsMaster": "XB-BOX - 🖼️ 图片参数大全", - "XB_MasterParameter": "XB-BOX - 🎛️ 全能参数控制", - "XB_UNetBlockSwap": "XB-BOX - ✂️ 模型分块交换(UNet)", - "XB_CheckpointBlockSwap": "XB-BOX - ✂️ 模型分块交换(checkpoints)", - "XB_DynamicBus": "XB-BOX - 🎛️ 动态总线插排", - "XB_UNetNameBroadcaster": "XB-BOX - 🗂️ UNet 名称分发", - "XB_CLIPNameBroadcaster": "XB-BOX - 🗂️ CLIP 名称分发", - "XB_Dashboard_Zen": "XB-BOX - 🪄 远程控制中心", - "XB_SamplerChunkMaster": "XB-BOX - 🧊 采样分块大师", - "XB_WanImageToVideo": "XB-BOX - 🖼️ 单图转视频分块 (Wan)", - "XB_WanFirstLastFrameToVideo": "XB-BOX - 🎞️ 首尾帧视频分块(Wan)", - "XB_BatchFolderLoader": "XB-BOX - 📂 图片批量加载", - "XB_Wan_ParamBus": "XB-BOX - 📦 视频参数总线", - "XB_Wan_RelayNode": "XB-BOX - 🏃 首尾帧接力点", - "XB_Video_Merger": "XB-BOX - 🧩 视频无缝拼接", - "XB_StoryboardSlicer": "XB-BOX - 🧊 分镜图片切割" + "XB_VRAM_Calculator": "XB-BOX - VRAM Calculator", + "XB_ChunkVisualization": "XB-BOX - Chunk Visualization", + "XTX_VRAM_Cleaner": "XB-BOX - VRAM Cleaner", + "XTX_Data_Radar": "XB-BOX - Data Radar", + "XB_VideoParamsMaster": "XB-BOX - Video Params Master", + "XB_ImageParamsMaster": "XB-BOX - Image Params Master", + "XB_MasterParameter": "XB-BOX - Master Parameter", + "XB_UNetBlockSwap": "XB-BOX - UNet Block Swap", + "XB_CheckpointBlockSwap": "XB-BOX - Checkpoint Block Swap", + "XB_DynamicBus": "XB-BOX - Dynamic Bus", + "XB_UNetNameBroadcaster": "XB-BOX - UNet Name Broadcaster", + "XB_CLIPNameBroadcaster": "XB-BOX - CLIP Name Broadcaster", + "XB_Dashboard_Zen": "XB-BOX - Dashboard Zen", + "XB_SamplerChunkMaster": "XB-BOX - Sampler Chunk Master", + "XB_WanImageToVideo": "XB-BOX - Wan Image2Video", + "XB_WanFirstLastFrameToVideo": "XB-BOX - Wan First/Last Frame2Video", + "XB_BatchFolderLoader": "XB-BOX - Batch Folder Loader", + "XB_Wan_ParamBus": "XB-BOX - Wan Param Bus", + "XB_Wan_RelayNode": "XB-BOX - Wan Relay Node", + "XB_Video_Merger": "XB-BOX - Video Merger", + "XB_StoryboardSlicer": "XB-BOX - Storyboard Slicer", + "XB_SageAttentionAccelerator": "XB-BOX - SageAttention Accelerator" } - # 打印超级醒目的成功提示 print_success("\n" + "="*50) - print_success("🚀 [XB-BOX] 小白工具箱 核心模块加载成功!") + print_success("🚀 [XB-BOX] XB_ToolBox Core Modules Loaded Successfully!") print_success("="*50 + "\n") except Exception as e: print_error("\n" + "="*50) - print_error("🚨 [XB-BOX] 致命错误:小白工具箱 加载失败!") - print_error(f"❌ 错误简述: {str(e)}") - print_warning("🔍 详细追踪信息如下 (请根据此处排查 Bug):") + print_error("🚨 [XB-BOX] FATAL ERROR: XB_ToolBox Loading Failed!") + print_error(f"❌ Error Detail: {str(e)}") traceback.print_exc() print_error("="*50 + "\n") diff --git a/js/folder_picker.js b/js/folder_picker.js index 6974b9e..96be380 100644 --- a/js/folder_picker.js +++ b/js/folder_picker.js @@ -1,34 +1,28 @@ import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; +const isZH = navigator.language.startsWith("zh"); + app.registerExtension({ name: "XB_ToolBox.FolderPicker", async beforeRegisterNodeDef(nodeType, nodeData, app) { - // 精准狙击我们的批量加载节点 if (nodeData.name === "XB_BatchFolderLoader") { const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; - - // 找到那个蠢笨的 directory 文本输入框 const dirWidget = this.widgets.find(w => w.name === "directory"); - // 在节点上凭空捏造一个高大上的“选择文件夹”按钮 - const btnWidget = this.addWidget("button", "📁 浏览文件夹...", "browse", () => { - - // 呼叫后端 Python 弹出 Windows 窗口 + const btnLabel = isZH ? "📁 浏览文件夹..." : "📁 Browse Folder..."; + const btnWidget = this.addWidget("button", btnLabel, "browse", () => { api.fetchApi("/xb_toolbox/choose_folder", { method: "POST" }) .then(res => res.json()) .then(data => { - // 如果用户选了路径,直接塞进文本框里 if (data.path) { dirWidget.value = data.path; } - }).catch(err => console.error("文件夹选择器唤醒失败:", err)); - + }).catch(err => {}); }); - return r; }; } diff --git a/js/xb_dashboard.js b/js/xb_dashboard.js index f642028..1c80fce 100644 --- a/js/xb_dashboard.js +++ b/js/xb_dashboard.js @@ -1,8 +1,5 @@ import { app } from "../../scripts/app.js"; -// ============================================================================== -// 🛠️ 核心工具:深度比较,防死循环 -// ============================================================================== function isValEqual(v1, v2) { if (v1 === v2) return true; if (typeof v1 === 'object' && typeof v2 === 'object') { @@ -12,27 +9,20 @@ function isValEqual(v1, v2) { return false; } -// ============================================================================== -// 🚨 存档清洗协议:清除上一版的魔改残留,恢复 100% 原生 Group 功能 -// ============================================================================== function cleanCorruptedGroups() { if (!app.graph) return; - const group = app.graph._groups.find(g => g.title === "🎮 XB 远程控制中心"); + const group = app.graph._groups.find(g => g.title === "🎮 XB Dashboard Zen"); if (group) { - // 删除我们之前强加的自定义绘画和属性,让引擎接管 if (group._xb_patched) { delete group.draw; delete group._xb_patched; } if (group.hasOwnProperty('recomputeInsideNodes')) { - delete group.recomputeInsideNodes; // 恢复原生拖拽抓取能力 + delete group.recomputeInsideNodes; } } } -// ============================================================================== -// 🛠️ 终极同步引擎:大动脉直连 + 强制继承 -// ============================================================================== function applyMirrorSyncLogic(proxyNode) { if (proxyNode._xb_sync_interval) return; @@ -41,7 +31,6 @@ function applyMirrorSyncLogic(proxyNode) { if (!realTarget) return; - // 强制继承初始值 if (proxyNode.widgets && realTarget.widgets) { for (let i = 0; i < proxyNode.widgets.length; i++) { let pw = proxyNode.widgets[i]; @@ -63,13 +52,13 @@ function applyMirrorSyncLogic(proxyNode) { const currentTarget = app.graph.getNodeById(targetId); if (!currentTarget) { - if (proxyNode.title !== "⚠️ 母节点已断开") { - proxyNode.title = "⚠️ 母节点已断开"; + if (proxyNode.title !== "⚠️ Mother Node Disconnected") { + proxyNode.title = "⚠️ Mother Node Disconnected"; proxyNode.color = "#880000"; proxyNode.setDirtyCanvas(true, true); } return; - } else if (proxyNode.title === "⚠️ 母节点已断开") { + } else if (proxyNode.title === "⚠️ Mother Node Disconnected") { proxyNode.title = "🪞 " + (currentTarget.title || currentTarget.type); if (currentTarget.color) proxyNode.color = currentTarget.color; else delete proxyNode.color; @@ -80,7 +69,6 @@ function applyMirrorSyncLogic(proxyNode) { let needRedraw = false; - // 大动脉直连出图 if (app.nodeOutputs && app.nodeOutputs[targetId]) { const motherOutput = app.nodeOutputs[targetId]; const proxyOutput = app.nodeOutputs[proxyNode.id]; @@ -93,7 +81,6 @@ function applyMirrorSyncLogic(proxyNode) { } } - // 图像指纹雷达 const getImgFingerprint = (imgs) => imgs ? imgs.map(img => img.src || "").join('|') + '|' + imgs.length : "null"; const currentImgState = getImgFingerprint(currentTarget.imgs); if (proxyNode._xb_last_img_state !== currentImgState) { @@ -105,7 +92,6 @@ function applyMirrorSyncLogic(proxyNode) { if (proxyNode.imageIndex !== currentTarget.imageIndex) { proxyNode.imageIndex = currentTarget.imageIndex; needRedraw = true; } if (proxyNode.animatedImages !== currentTarget.animatedImages) { proxyNode.animatedImages = currentTarget.animatedImages; needRedraw = true; } - // 参数双向绑定 if (proxyNode.widgets && currentTarget.widgets) { for (let i = 0; i < proxyNode.widgets.length; i++) { let pw = proxyNode.widgets[i]; @@ -167,7 +153,6 @@ function applyMirrorSyncLogic(proxyNode) { }; } -// 读档拦截恢复影子 const origOnNodeAdded = LGraph.prototype.add; LGraph.prototype.add = function(node, skip_compute_order) { origOnNodeAdded.apply(this, arguments); @@ -179,9 +164,6 @@ LGraph.prototype.add = function(node, skip_compute_order) { } }; -// ============================================================================== -// 📦 原生组件选择弹窗 -// ============================================================================== function showMultiPickerModal(validNodes, onConfirmCallback) { const existing = document.getElementById("xb-picker-modal"); if (existing) existing.remove(); @@ -208,7 +190,7 @@ function showMultiPickerModal(validNodes, onConfirmCallback) { const header = document.createElement("div"); header.style.cssText = "padding: 15px 20px; background: #1c232d; border-bottom: 1px solid #334; display: flex; justify-content: space-between; align-items: center;"; - header.innerHTML = `📦 批量选择要克隆的组件`; + header.innerHTML = `📦 Batch Select Components to Clone`; const closeBtn = document.createElement("button"); closeBtn.innerText = "✖"; @@ -222,7 +204,7 @@ function showMultiPickerModal(validNodes, onConfirmCallback) { const checkboxes = []; if (validNodes.length === 0) { - listContainer.innerHTML = `
当前工作流无可用节点。
`; + listContainer.innerHTML = `
No available nodes in current workflow.
`; } else { validNodes.forEach(node => { const label = document.createElement("label"); @@ -241,7 +223,7 @@ function showMultiPickerModal(validNodes, onConfirmCallback) { textDiv.style.cssText = "display: flex; flex-direction: column;"; textDiv.innerHTML = ` ${node.title || node.type} - ID: ${node.id} | 类型: ${node.type} + ID: ${node.id} | Type: ${node.type} `; label.appendChild(cb); @@ -257,7 +239,7 @@ function showMultiPickerModal(validNodes, onConfirmCallback) { footer.style.cssText = "padding: 15px 20px; background: #1c232d; border-top: 1px solid #334; display: flex; justify-content: space-between; align-items: center;"; const selectAllBtn = document.createElement("button"); - selectAllBtn.innerText = "☑ 全选 / 反选"; + selectAllBtn.innerText = "☑ Select All / Invert"; selectAllBtn.style.cssText = "padding: 8px 15px; background: #2a3441; color: white; border: none; border-radius: 4px; cursor: pointer;"; selectAllBtn.onclick = (e) => { e.preventDefault(); @@ -266,7 +248,7 @@ function showMultiPickerModal(validNodes, onConfirmCallback) { }; const confirmBtn = document.createElement("button"); - confirmBtn.innerText = "🚀 执行克隆"; + confirmBtn.innerText = "🚀 Execute Clone"; confirmBtn.style.cssText = "padding: 8px 20px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;"; confirmBtn.onclick = (e) => { @@ -290,9 +272,6 @@ function showMultiPickerModal(validNodes, onConfirmCallback) { document.body.appendChild(overlay); } -// ============================================================================== -// 🚀 核心控制台:100% 拥抱原生 API,拒绝任何底层魔改 -// ============================================================================== app.registerExtension({ name: "XB_ToolBox.Dashboard", @@ -313,7 +292,6 @@ app.registerExtension({ if (info && info.widgets_values) info.widgets_values = []; if (origConfigure) origConfigure.apply(this, arguments); - // 启动清洗协议:把旧存档里被污染的组洗白 setTimeout(() => cleanCorruptedGroups(), 200); }; @@ -321,14 +299,14 @@ app.registerExtension({ nodeType.prototype.onNodeCreated = function () { if (onNodeCreated) onNodeCreated.apply(this, arguments); - this.title = "🪄 XB 远程控制中心"; + this.title = "🪄 XB Dashboard Zen"; this.properties = this.properties || {}; - this.addWidget("button", "➕ 克隆组件", "extract", () => { + this.addWidget("button", "➕ Clone Component", "extract", () => { setTimeout(() => this.openExtractModal(), 15); }); - this.addWidget("button", "🧹 清除组件", "clear", () => { + this.addWidget("button", "🧹 Clear Components", "clear", () => { setTimeout(() => this.destroyAllMirrors(), 15); }); @@ -349,15 +327,13 @@ app.registerExtension({ }; nodeType.prototype.createMirrorsInPlace = function(nodesToClone) { - // 1. 完全调用官方 API 创建原生组 - let group = app.graph._groups.find(g => g.title === "🎮 XB 远程控制中心"); + let group = app.graph._groups.find(g => g.title === "🎮 XB Dashboard Zen"); if (!group) { group = new LiteGraph.LGraphGroup(); - group.title = "🎮 XB 远程控制中心"; + group.title = "🎮 XB Dashboard Zen"; app.graph.add(group); } - // 2. 根据中枢节点计算坐标 let newGroupX = this.pos[0] - 40; let newGroupY = this.pos[1] - 100; @@ -378,7 +354,6 @@ app.registerExtension({ }); } - // 3. 克隆节点 nodesToClone.forEach(target => { const existingProxy = existingProxies.find(n => n.properties.xb_mirror_target_id === target.id); if (existingProxy) return; @@ -392,7 +367,6 @@ app.registerExtension({ proxy.isVirtualNode = true; proxy.title = "🪞 " + (target.title || target.type); - // 完全继承原生颜色 if (target.color) proxy.color = target.color; if (target.bgcolor) proxy.bgcolor = target.bgcolor; @@ -412,11 +386,9 @@ app.registerExtension({ applyMirrorSyncLogic(proxy); }); - // 4. 给原生组赋值尺寸 group.pos = [newGroupX, newGroupY]; group.size = [maxRightEdge - newGroupX + 40, maxBottomEdge - newGroupY + 40]; - // 5. 🚨 终极原生奥义:直接调用 LiteGraph 原生的重算方法,让引擎自己去抓节点、写内存! if (group.recomputeInsideNodes) { group.recomputeInsideNodes(); } @@ -426,11 +398,11 @@ app.registerExtension({ }; nodeType.prototype.destroyAllMirrors = function() { - if (confirm("确定要销毁所有克隆组件吗?(原工作流完全不受影响)")) { + if (confirm("Are you sure you want to destroy all cloned components? (Original workflow remains unaffected)")) { const proxies = app.graph._nodes.filter(n => n.properties && n.properties.xb_mirror_target_id); proxies.forEach(p => app.graph.remove(p)); - const group = app.graph._groups.find(g => g.title === "🎮 XB 远程控制中心"); + const group = app.graph._groups.find(g => g.title === "🎮 XB Dashboard Zen"); if (group) app.graph.remove(group); app.graph.change(); diff --git a/js/xb_master_param.js b/js/xb_master_param.js index 55d87d8..784f2cd 100644 --- a/js/xb_master_param.js +++ b/js/xb_master_param.js @@ -1,15 +1,12 @@ import { app } from "../../scripts/app.js"; -// 📝 架构规则表:彻底分离,绝不互相污染 -// dragStep: 控制拖拽速度和箭头增量 (原生会乘0.1) -// snapRound: 控制吸附刻度 const MODE_CONFIGS = { - "自由模式": { min: 0, max: 9999, dragStep: 10, snapRound: 1, precision: 0, def: 0 }, - "模型模式": { min: 0, max: 50, dragStep: 10, snapRound: 1, precision: 0, def: 0 }, - "编码模式": { min: 64, max: 3840, dragStep: 320, snapRound: 32, precision: 0, def: 64 }, - "解码模式": { min: 64, max: 3840, dragStep: 320, snapRound: 32, precision: 0, def: 64 }, - "比例模式": { min: 0.00, max: 1.00, dragStep: 0.1, snapRound: 0.01, precision: 2, def: 0.50 }, - "其他模式": { min: -99999.00, max: 99999.00, dragStep: 0.1, snapRound: 0.01, precision: 2, def: 1.00 } + "Free Mode": { min: 0, max: 9999, dragStep: 10, snapRound: 1, precision: 0, def: 0 }, + "Model Mode": { min: 0, max: 50, dragStep: 10, snapRound: 1, precision: 0, def: 0 }, + "Encode Mode": { min: 64, max: 3840, dragStep: 320, snapRound: 32, precision: 0, def: 64 }, + "Decode Mode": { min: 64, max: 3840, dragStep: 320, snapRound: 32, precision: 0, def: 64 }, + "Ratio Mode": { min: 0.00, max: 1.00, dragStep: 0.1, snapRound: 0.01, precision: 2, def: 0.50 }, + "Other Mode": { min: -99999.00, max: 99999.00, dragStep: 0.1, snapRound: 0.01, precision: 2, def: 1.00 } }; app.registerExtension({ @@ -20,11 +17,8 @@ app.registerExtension({ const wValueOrig = node.widgets.find(w => w.name === "value"); if (wMode && wValueOrig) { - // 1. 记录原位置,确保替换时不会跑到最下面 const valueWidgetIndex = node.widgets.indexOf(wValueOrig); - // 2. 核心架构:预先制造 6 个纯正的原生 Widget (影子分身) - // 它们在创建的一瞬间就拥有了最完美的 step 和 round,再也不会被篡改 node._xb_shadow_widgets = {}; for (const [mName, cfg] of Object.entries(MODE_CONFIGS)) { const w = node.addWidget("number", "value", cfg.def, function(v){}, { @@ -34,38 +28,31 @@ app.registerExtension({ round: cfg.snapRound, precision: cfg.precision }); - // 创建完立刻从面板上摘下来,放进后台仓库 node._xb_shadow_widgets[mName] = node.widgets.pop(); } - // 3. 定义无缝换挡 (组件热替换) 核心逻辑 node.swapWidget = function(modeName) { - // 卸载当前面板上的 value 组件 const currIdx = node.widgets.findIndex(w => w.name === "value"); if (currIdx > -1) { node.widgets.splice(currIdx, 1); } - // 从仓库中取出对应的组件,精准安插回原来的位置 const targetW = node._xb_shadow_widgets[modeName]; if (targetW) { - targetW.name = "value"; // 必须叫 value,否则 Python 后端读不到 + targetW.name = "value"; node.widgets.splice(valueWidgetIndex, 0, targetW); } app.graph.setDirtyCanvas(true, true); }; - // 4. 初次加载:立刻换上当前模式的专属组件 node.swapWidget(wMode.value); - // 5. 监听下拉菜单切换:不再修改数字,而是直接“换人”! const origModeCb = wMode.callback; wMode.callback = function(v) { node.swapWidget(v); if (origModeCb) return origModeCb.apply(this, arguments); }; - // 6. 完美兼容 ComfyUI 的工作流加载 (F5 刷新或拖入图片时恢复数据) const onConfigure = node.onConfigure; node.onConfigure = function(info) { if (onConfigure) onConfigure.apply(this, arguments); @@ -73,7 +60,6 @@ app.registerExtension({ if (wMode.value) { node.swapWidget(wMode.value); - // 强制将加载的数据覆盖到换上来的组件里 if (info && info.widgets_values && info.widgets_values.length > valueWidgetIndex) { const savedVal = info.widgets_values[valueWidgetIndex]; const activeW = node.widgets.find(w => w.name === "value"); diff --git a/js/xb_smart_ui.js b/js/xb_smart_ui.js index 86a7dc6..79034da 100644 --- a/js/xb_smart_ui.js +++ b/js/xb_smart_ui.js @@ -1,5 +1,7 @@ import { app } from "../../scripts/app.js"; +const isZH = navigator.language.startsWith("zh"); + app.registerExtension({ name: "XB_ToolBox.SmartUI", async beforeRegisterNodeDef(nodeType, nodeData, app) { @@ -7,7 +9,6 @@ app.registerExtension({ nodeType.prototype.updateWidgetState = function () { const optInput = this.inputs?.find(i => i.name === "opt_end_image"); - // 🔴 核心修复:只认原始名字,绝不乱改名 const imgCombo = this.widgets?.find(w => w.name === "end_image_file"); let uploadBtn = this.widgets?.find(w => w.type === "button" && !w.name.includes("randomize")); @@ -16,21 +17,18 @@ app.registerExtension({ const isConnected = !!optInput.link; if (isConnected) { - // 🔒 连线状态:只变灰,绝对不准改 imgCombo.name !(否则会导致后端报错) imgCombo.disabled = true; - // 按钮可以随意改名,因为它不负责往后端发数据 if (!uploadBtn._orig_callback) uploadBtn._orig_callback = uploadBtn.callback; - uploadBtn.name = "🔒 端口已被连线接管"; - uploadBtn.label = "🔒 端口已被连线接管"; + uploadBtn.name = isZH ? "🔒 端口已被连线接管" : "Locked by connection"; + uploadBtn.label = isZH ? "🔒 端口已被连线接管" : "Locked by connection"; uploadBtn.callback = null; } else { - // 🔓 断线状态:恢复使用 imgCombo.disabled = false; - uploadBtn.name = "选择上传尾帧图片"; - uploadBtn.label = "选择上传尾帧图片"; + uploadBtn.name = isZH ? "选择上传尾帧图片" : "Select/Upload End Frame"; + uploadBtn.label = isZH ? "选择上传尾帧图片" : "Select/Upload End Frame"; if (uploadBtn._orig_callback) uploadBtn.callback = uploadBtn._orig_callback; } this.setDirtyCanvas(true, true); diff --git a/js/xb_video.js b/js/xb_video.js index 2bbf449..cb07166 100644 --- a/js/xb_video.js +++ b/js/xb_video.js @@ -1,8 +1,7 @@ import { app } from "../../scripts/app.js"; -console.log("🚀 XB-Tools: 媒体参数节点 JS (原版交互逻辑 100% 保留 + 0.20.X 兼容版) 已加载!"); +const isZH = navigator.language.startsWith("zh"); -// 🟢 0.20.X 兼容层:新版 UI 渲染引擎改动了 DOM 结构,这里做静默兼容,绝不干预任何逻辑! const xb_dispatch = (w, val) => { if (w.inputEl) { w.inputEl.value = val; @@ -22,10 +21,6 @@ app.registerExtension({ if (!app.graph || !app.graph._nodes) return; for (const node of app.graph._nodes) { - - // ========================================== - // 🎬 核心 1:视频参数大全 (XB_VideoParamsMaster) - // ========================================== if (node.comfyClass === "XB_VideoParamsMaster" && node.widgets) { const wRatio = node.widgets.find(w => w.name === "aspect_ratio"); const wDisp = node.widgets.find(w => w.name === "duration_display"); @@ -36,8 +31,6 @@ app.registerExtension({ const wHeight = node.widgets.find(w => w.name === "height"); if (wDisp && wLen && wFps && wFpsF && wWidth && wHeight && wRatio) { - - // 1. 样式与初始化 (加入 .element 兼容) let dispEl = wDisp.inputEl || wDisp.element; if (dispEl && dispEl.style && dispEl.style.backgroundColor !== "rgb(34, 34, 34)") { dispEl.readOnly = true; @@ -59,11 +52,10 @@ app.registerExtension({ let needsUpdate = false; let valRatio = wRatio.value; - let isFree = valRatio.includes("自由"); + let isFree = valRatio.includes("Free"); let rChanged = valRatio !== node._xb_last_ratio; let isGoldenZone = (valRatio === "16:9" || valRatio === "9:16"); - // 2. 视频档位与步长吸附 let currentStep = isGoldenZone ? 1 : (isFree ? 1 : 16); wWidth.options.step = currentStep; wHeight.options.step = currentStep; if (wWidth.inputEl) wWidth.inputEl.step = currentStep; else if (wWidth.element) wWidth.element.step = currentStep; @@ -144,7 +136,6 @@ app.registerExtension({ needsUpdate = true; } - // 3. FPS 与视频时长联动 if (wFps.value !== node._xb_last_fps) { let val = Math.round(Number(wFps.value)); wFps.value = val; wFpsF.value = val; node._xb_last_fps = val; node._xb_last_fps_float = val; @@ -179,7 +170,7 @@ app.registerExtension({ let fps = parseInt(wFps.value, 10) || 1; let seconds = ((safeLen - 1) / fps).toFixed(2); - let expectedText = `视频时长: ${seconds} 秒`; + let expectedText = isZH ? `视频时长: ${seconds} 秒` : `Video Duration: ${seconds} s`; if (wDisp.value !== expectedText) { wDisp.value = expectedText; xb_dispatch(wDisp, expectedText); @@ -192,9 +183,6 @@ app.registerExtension({ } } - // ========================================== - // 🖼️ 核心 2:图片参数大全 (XB_ImageParamsMaster) - // ========================================== if (node.comfyClass === "XB_ImageParamsMaster" && node.widgets) { const wRatio = node.widgets.find(w => w.name === "aspect_ratio"); const wWidth = node.widgets.find(w => w.name === "width"); @@ -210,10 +198,9 @@ app.registerExtension({ let needsUpdate = false; let valRatio = wRatio.value; - let isFree = valRatio.includes("自由"); + let isFree = valRatio.includes("Free"); let rChanged = valRatio !== node._xb_last_ratio; - // 1. 宽高步长安全引擎 let currentStep = isFree ? 1 : 16; wWidth.options.step = currentStep; wHeight.options.step = currentStep; if (wWidth.inputEl) wWidth.inputEl.step = currentStep; else if (wWidth.element) wWidth.element.step = currentStep; diff --git a/js/xb_wiring.js b/js/xb_wiring.js index d2ad82c..c08de4e 100644 --- a/js/xb_wiring.js +++ b/js/xb_wiring.js @@ -1,8 +1,7 @@ import { app } from "../../scripts/app.js"; -// ============================================================================== -// 独立模块:深色工业风自定义弹窗 -// ============================================================================== +const isZH = navigator.language.startsWith("zh"); + function showCustomPrompt(title, defaultValue, callback) { const existing = document.getElementById("xb-custom-prompt"); if (existing) existing.remove(); @@ -40,13 +39,13 @@ function showCustomPrompt(title, defaultValue, callback) { btnContainer.style.cssText = "display: flex; justify-content: flex-end; gap: 12px;"; const btnCancel = document.createElement("button"); - btnCancel.innerText = "取消"; + btnCancel.innerText = isZH ? "取消" : "Cancel"; btnCancel.style.cssText = "padding: 8px 16px; background: transparent; border: 1px solid #666; color: #ccc; border-radius: 4px; cursor: pointer; font-size: 13px; transition: 0.2s;"; btnCancel.onmouseover = () => { btnCancel.style.background = "#333"; }; btnCancel.onmouseout = () => { btnCancel.style.background = "transparent"; }; const btnOk = document.createElement("button"); - btnOk.innerText = "确定"; + btnOk.innerText = isZH ? "确定" : "OK"; btnOk.style.cssText = "padding: 8px 16px; background: #4CAF50; border: none; color: #fff; border-radius: 4px; cursor: pointer; font-size: 13px; transition: 0.2s;"; btnOk.onmouseover = () => { btnOk.style.background = "#45a049"; }; btnOk.onmouseout = () => { btnOk.style.background = "#4CAF50"; }; @@ -95,16 +94,11 @@ app.registerExtension({ this.title = ""; this.color = "#151e29"; this.bgcolor = "#0B1116"; - // 初始化极度压缩的尺寸 this.size = [90, 46]; }; - // ========================================== - // 【究极榨干法】:高度死锁公式极限压缩 - // ========================================== const getExactHeight = (node) => { const rows = Math.max(node.inputs ? node.inputs.length : 0, node.outputs ? node.outputs.length : 0); - // 30(标题高) + 孔位总高 + 16(极限底部留白,原先是28) return 30 + rows * LiteGraph.NODE_SLOT_HEIGHT + 16; }; @@ -113,8 +107,8 @@ app.registerExtension({ }; nodeType.prototype.onResize = function(size) { - size[1] = getExactHeight(this); // 高度绝对死锁 - if (size[0] < 90) size[0] = 90; // 宽度防崩 + size[1] = getExactHeight(this); + if (size[0] < 90) size[0] = 90; }; const onConnectionsChange = nodeType.prototype.onConnectionsChange; @@ -165,11 +159,9 @@ app.registerExtension({ ctx.textBaseline = "middle"; const centerX = this.size[0] / 2; - // 按钮核心位置极限上提!距底部仅 10 像素 const btnY = this.size[1] - 10; const btnInterval = 28; - // 深色护盾也同步压缩高度 (18像素高,完美包裹字体) const baseW = btnInterval * 2 + 28; ctx.fillStyle = "#0B1116"; ctx.beginPath(); @@ -177,7 +169,6 @@ app.registerExtension({ ctx.fill(); ctx.font = "16px Arial"; - // 调整视觉重心,让 emoji 看起来在护盾的正中央 ctx.fillStyle = "#4CAF50"; ctx.fillText("➕", centerX - btnInterval, btnY); ctx.fillStyle = "#FFC107"; ctx.fillText("⭕", centerX, btnY); ctx.fillStyle = "#F44336"; ctx.fillText("➖", centerX + btnInterval, btnY); @@ -188,7 +179,6 @@ app.registerExtension({ const onMouseDown = nodeType.prototype.onMouseDown; nodeType.prototype.onMouseDown = function(e, pos, canvas) { const centerX = this.size[0] / 2; - // Hitbox 同步极限上提 const btnY = this.size[1] - 10; const btnInterval = 28; @@ -216,7 +206,7 @@ app.registerExtension({ const idx = this.portCount - 1; if ((this.inputs[idx] && this.inputs[idx].link !== null) || (this.outputs[idx] && this.outputs[idx].links && this.outputs[idx].links.length > 0)) { - alert("【防呆设计】该通道上还有连线,请拔除线缆后再移除通道!"); + alert(isZH ? "【防呆设计】该通道上还有连线,请拔除线缆后再移除通道!" : "[Safety] Wires are still connected to this channel. Please disconnect before removing!"); } else { this.removeInput(idx); this.removeOutput(idx); @@ -250,8 +240,9 @@ app.registerExtension({ if (clickedRow >= 0) { const currentType = this.inputs[clickedRow].customType || "*"; + const promptTitle = isZH ? `📌 修改通道 ${clickedRow + 1} 的自定义类型:\n(留空则恢复为通配符 ANY)` : `[Edit Type] Channel ${clickedRow + 1}:\n(Leave blank to restore ANY wildcard)`; showCustomPrompt( - `📌 修改通道 ${clickedRow + 1} 的自定义类型:\n(留空则恢复为通配符 ANY)`, + promptTitle, currentType === "*" ? "" : currentType, (newType) => { const t = newType.trim() === "" ? "*" : newType.toUpperCase(); diff --git a/locales/zh/main.json b/locales/zh/main.json new file mode 100644 index 0000000..7f74839 --- /dev/null +++ b/locales/zh/main.json @@ -0,0 +1,49 @@ +{ + "nodeCategories": { + "XB_ToolBox": "小白工具箱", + "Pipeline": "流水线架构", + "Wiring": "布线整理", + "VRAM_Hacks": "显存特攻", + "Dashboard": "总控中心", + "Image_Params": "图像参数", + "Params_Control": "参数控制", + "Tile_Tools": "分块工具", + "Batch_Process": "批量处理" + }, + "Mode A (128x128x32)": "模式A (128x128x32)", + "Mode B (128x64x96)": "模式 B (128x64x96)", + "Mode C (128x16x16)": "模式 C (128x16x16)", + "Mode D (64x64x16)": "模式 D (64x64x16)", + "Full Preview": "完整预览", + "Chunks Only": "仅显示分块", + "Spatial Only": "仅画面", + "Temporal Only": "仅时间", + "Spatial & Temporal": "画面与时间", + "Encode Stage": "编码阶段", + "Decode Stage": "解码阶段", + "Free": "自由 (Free)", + "Free Mode": "自由模式", + "Model Mode": "模型模式", + "Encode Mode": "编码模式", + "Decode Mode": "解码模式", + "Ratio Mode": "比例模式", + "Other Mode": "其他模式", + "3-Grid (1x3 Horizontal)": "三宫格 (1行3列 · 横向)", + "3-Grid (3x1 Vertical)": "三宫格 (3行1列 · 竖向)", + "4-Grid (2x2)": "四宫格 (2行2列)", + "6-Grid (2x3 Horizontal)": "六宫格 (2行3列 · 横向)", + "6-Grid (3x2 Vertical)": "六宫格 (3行2列 · 竖向)", + "9-Grid (3x3)": "九宫格 (3行3列)", + "Browse Folder...": "📁 浏览文件夹...", + "Mother Node Disconnected": "⚠️ 母节点已断开", + "Mirror: ": "🪞 ", + "Batch Select Components": "📦 批量选择要克隆的组件", + "Select All / Invert": "☑ 全选 / 反选", + "Execute Clone": "🚀 执行克隆", + "Are you sure you want to destroy all cloned components?": "确定要销毁所有克隆组件吗?", + "No available nodes.": "当前工作流无可用节点。", + "Locked by connection": "🔒 端口已被连线接管", + "Select/Upload End Frame": "选择上传尾帧图片", + "Cancel": "取消", + "OK": "确定" +} \ No newline at end of file diff --git a/locales/zh/nodeDefs.json b/locales/zh/nodeDefs.json new file mode 100644 index 0000000..c1224d8 --- /dev/null +++ b/locales/zh/nodeDefs.json @@ -0,0 +1,321 @@ +{ + "XB_VRAM_Calculator": { + "display_name": "XB-BOX - 📟 可用显存计算", + "inputs": { + "Total_VRAM_GB": { "name": "显卡总显存_GB" }, + "System_Overhead_GB": { "name": "系统开销_GB" }, + "Main_Video_Model": { "name": "视频主模型" }, + "Model_Quantization": { "name": "模型量化" }, + "Total_Model_Layers": { "name": "主模型总层数" }, + "Layers_to_Swap": { "name": "下放层数" }, + "LoRA_Plugin_VRAM_GB": { "name": "LoRA及插件总占用_GB" }, + "Context_Length_Frames": { "name": "上下文总帧数" }, + "Spatial_Resolution": { "name": "空间分辨率" } + }, + "outputs": { + "0": { "name": "可用显存输出" } + } + }, + "XB_ChunkVisualization": { + "display_name": "XB-BOX - 🧊 时空分块预览", + "inputs": { + "Stat_Mode": { "name": "统计模式" }, + "Available_VRAM_GB": { "name": "可用显存_GB" }, + "Image_Width": { "name": "图片宽度" }, + "Image_Height": { "name": "图片高度" }, + "Image_Frames": { "name": "图像帧数" }, + "Current_Stage": { "name": "当前阶段" }, + "Spatial_Tile_Size": { "name": "分块大小" }, + "Spatial_Tile_Overlap": { "name": "分块重叠" }, + "Temporal_Chunk_Size": { "name": "切片大小" }, + "Temporal_Chunk_Overlap": { "name": "切片重叠" }, + "Spatial_Tiling": { "name": "空间分块" }, + "Temporal_Chunking": { "name": "时间切片" }, + "Image_Input": { "name": "图像输入" } + }, + "outputs": { + "0": { "name": "预览图" }, + "1": { "name": "管道数量" }, + "2": { "name": "单管像素量" }, + "3": { "name": "上下文峰值" } + } + }, + "XTX_VRAM_Cleaner": { + "display_name": "XB-BOX - 🧹 显存清理大师", + "inputs": { + "Nuclear_Cleanup": { "name": "核爆级清理" }, + "model": { "name": "模型" }, + "positive": { "name": "正面提示词" }, + "negative": { "name": "负面提示词" }, + "latent_image": { "name": "潜变量" } + }, + "outputs": { + "0": { "name": "模型" }, + "1": { "name": "正面提示词" }, + "2": { "name": "负面提示词" }, + "3": { "name": "潜变量" } + } + }, + "XTX_Data_Radar": { + "display_name": "XB-BOX - 🪞 生成数据预览", + "inputs": { + "model": { "name": "模型" }, + "latent": { "name": "潜变量" }, + "positive_cond": { "name": "正面条件" }, + "negative_cond": { "name": "负面条件" } + }, + "outputs": { + "0": { "name": "模型" }, + "1": { "name": "潜变量" } + } + }, + "XB_VideoParamsMaster": { + "display_name": "XB-BOX - 🎬 视频参数大全", + "inputs": { + "aspect_ratio": { "name": "画幅比例" }, + "duration_display": { "name": "时长估算" }, + "width": { "name": "宽度" }, + "height": { "name": "高度" }, + "length": { "name": "帧数" }, + "fps": { "name": "帧率" }, + "fps_float": { "name": "帧率(小数)" } + }, + "outputs": { + "0": { "name": "宽度" }, + "1": { "name": "高度" }, + "2": { "name": "帧数" }, + "3": { "name": "帧率" }, + "4": { "name": "帧率(小数)" }, + "5": { "name": "缩放尺寸" } + } + }, + "XB_ImageParamsMaster": { + "display_name": "XB-BOX - 🖼️ 图片参数大全", + "inputs": { + "aspect_ratio": { "name": "画幅比例" }, + "width": { "name": "宽度" }, + "height": { "name": "高度" }, + "batch_size": { "name": "生成数量" }, + "strength_float": { "name": "强度(小数)" }, + "strength_int": { "name": "强度(整数)" } + }, + "outputs": { + "0": { "name": "图片宽度" }, + "1": { "name": "图片高度" }, + "2": { "name": "生成数量" }, + "3": { "name": "浮点控制" }, + "4": { "name": "整数控制" }, + "5": { "name": "缩放尺寸" } + } + }, + "XB_MasterParameter": { + "display_name": "XB-BOX - 🎛️ 全能参数控制", + "inputs": { + "mode": { "name": "控制模式" }, + "value": { "name": "数值" } + }, + "outputs": { + "0": { "name": "整数(INT)" }, + "1": { "name": "浮点(FLOAT)" } + } + }, + "XB_UNetBlockSwap": { + "display_name": "XB-BOX - ✂️ 模型分块交换(UNet)", + "inputs": { + "unet_model": { "name": "UNet模型" }, + "blocks_to_swap": { "name": "下放块数量" } + }, + "outputs": { + "0": { "name": "UNet模型" } + } + }, + "XB_CheckpointBlockSwap": { + "display_name": "XB-BOX - ✂️ 模型分块交换(Checkpoints)", + "inputs": { + "checkpoint_model": { "name": "大模型" }, + "blocks_to_swap": { "name": "下放块数量" }, + "offload_img_emb": { "name": "卸载图像嵌入层" }, + "offload_txt_emb": { "name": "卸载文本嵌入层" }, + "use_non_blocking": { "name": "启用非阻塞传输" } + }, + "outputs": { + "0": { "name": "大模型" } + } + }, + "XB_DynamicBus": { + "display_name": "XB-BOX - 🎛️ 动态总线插排" + }, + "XB_UNetNameBroadcaster": { + "display_name": "XB-BOX - 🗂️ UNet 名称分发", + "inputs": { + "unet_name": { "name": "选择UNet模型" } + }, + "outputs": { + "0": { "name": "UNet名称" } + } + }, + "XB_CLIPNameBroadcaster": { + "display_name": "XB-BOX - 🗂️ CLIP 名称分发", + "inputs": { + "clip_name": { "name": "选择CLIP模型" } + }, + "outputs": { + "0": { "name": "CLIP名称" } + } + }, + "XB_Dashboard_Zen": { + "display_name": "XB-BOX - 🪄 远程控制中心" + }, + "XB_SamplerChunkMaster": { + "display_name": "XB-BOX - 🧊 采样分块大师", + "inputs": { + "model": { "name": "模型" }, + "stat_mode": { "name": "状态模式" }, + "tile_size": { "name": "分块尺寸" }, + "tile_overlap": { "name": "分块重叠" }, + "frame_chunk_size": { "name": "时间切片大小" }, + "frame_chunk_overlap": { "name": "时间切片重叠" }, + "rocm_optimized": { "name": "ROCm显存优化" }, + "latent_info": { "name": "潜变量信息" } + }, + "outputs": { + "0": { "name": "模型" }, + "1": { "name": "预览图" } + } + }, + "XB_WanImageToVideo": { + "display_name": "XB-BOX - 🖼️ 单图转视频分块 (Wan)", + "inputs": { + "positive": { "name": "正面提示词" }, + "negative": { "name": "负面提示词" }, + "vae": { "name": "VAE解码器" }, + "width": { "name": "宽度" }, + "height": { "name": "高度" }, + "length": { "name": "帧数" }, + "batch_size": { "name": "生成数量" }, + "vae_tile_size": { "name": "VAE分块大小" }, + "clip_vision_output": { "name": "CLIP视觉特征" }, + "start_image": { "name": "首帧图片" } + }, + "outputs": { + "0": { "name": "正面提示词" }, + "1": { "name": "负面提示词" }, + "2": { "name": "潜变量" } + } + }, + "XB_WanFirstLastFrameToVideo": { + "display_name": "XB-BOX - 🎞️ 首尾帧视频分块(Wan)", + "inputs": { + "positive": { "name": "正面提示词" }, + "negative": { "name": "负面提示词" }, + "vae": { "name": "VAE解码器" }, + "width": { "name": "宽度" }, + "height": { "name": "高度" }, + "length": { "name": "帧数" }, + "batch_size": { "name": "生成数量" }, + "vae_tile_size": { "name": "VAE分块大小" }, + "clip_vision_start_image": { "name": "首帧视觉特征" }, + "clip_vision_end_image": { "name": "尾帧视觉特征" }, + "start_image": { "name": "首帧图片" }, + "end_image": { "name": "尾帧图片" } + }, + "outputs": { + "0": { "name": "正面提示词" }, + "1": { "name": "负面提示词" }, + "2": { "name": "潜变量" } + } + }, + "XB_BatchFolderLoader": { + "display_name": "XB-BOX - 📂 图片批量加载", + "inputs": { + "directory": { "name": "文件夹路径" }, + "seed": { "name": "步进种子" } + }, + "outputs": { + "0": { "name": "图像" }, + "1": { "name": "遮罩" }, + "2": { "name": "文件名" }, + "3": { "name": "总数量" } + } + }, + "XB_Wan_ParamBus": { + "display_name": "XB-BOX - 📦 视频参数总线", + "inputs": { + "model_high": { "name": "高噪模型" }, + "model_low": { "name": "低噪模型" }, + "clip": { "name": "CLIP模型" }, + "vae": { "name": "VAE模型" }, + "negative_prompt": { "name": "负面提示词" }, + "width": { "name": "宽度" }, + "height": { "name": "高度" }, + "length": { "name": "帧数" }, + "fps": { "name": "帧率" }, + "vae_tile_size": { "name": "VAE分块大小" }, + "steps": { "name": "总步数" }, + "high_noise_steps": { "name": "高噪步数" }, + "cfg": { "name": "引导系数" }, + "sampler_name": { "name": "采样器" }, + "scheduler": { "name": "调度器" } + }, + "outputs": { + "0": { "name": "📦 WAN_BUS (动态总线)" } + } + }, + "XB_Wan_RelayNode": { + "display_name": "XB-BOX - 🏃 首尾帧接力点", + "inputs": { + "wan_bus": { "name": "WAN动态总线" }, + "start_image": { "name": "起始图片" }, + "positive_prompt": { "name": "段落正向提示词" }, + "seed": { "name": "随机种" }, + "cut_first_frame": { "name": "去除首帧(接力去重)" }, + "end_image_file": { "name": "选择尾图" }, + "opt_end_image": { "name": "可选尾图连线" }, + "prev_video": { "name": "前段视频" } + }, + "outputs": { + "0": { "name": "📦 WAN_BUS (传给下段)" }, + "1": { "name": "🖼️ 当前尾图 (接下段首图)" }, + "2": { "name": "🎞️ 累加视频流" } + } + }, + "XB_Video_Merger": { + "display_name": "XB-BOX - 🧩 视频无缝拼接", + "inputs": { + "video_1": { "name": "视频段 1" }, + "video_2": { "name": "视频段 2" } + }, + "outputs": { + "0": { "name": "🎞️ 完整长视频" } + } + }, + "XB_StoryboardSlicer": { + "display_name": "XB-BOX - 🧊 分镜图片切割", + "inputs": { + "image": { "name": "图像" }, + "mode": { "name": "切分模式" }, + "crop_margin": { "name": "边缘裁剪比例" } + }, + "outputs": { + "0": { "name": "图1" }, + "1": { "name": "图2" }, + "2": { "name": "图3" }, + "3": { "name": "图4" }, + "4": { "name": "图5" }, + "5": { "name": "图6" }, + "6": { "name": "图7" }, + "7": { "name": "图8" }, + "8": { "name": "图9" } + } + }, + "XB_SageAttentionAccelerator": { + "display_name": "XB-BOX - 🚀 Sage注意力加速", + "inputs": { + "model": { "name": "模型" }, + "preset": { "name": "加速档位预设" } + }, + "outputs": { + "0": { "name": "模型" } + } + } +} \ No newline at end of file diff --git a/nodes_batch.py b/nodes_batch.py index f0c62ae..d7e856c 100644 --- a/nodes_batch.py +++ b/nodes_batch.py @@ -7,27 +7,23 @@ import folder_paths import datetime class XB_BatchFolderLoader: - """ - 文件夹图片接力加载节点 (架构优化版) - 支持自动归档路径输出与友好数量提示 - """ @classmethod def INPUT_TYPES(s): return { "required": { "directory": ("STRING", {"default": ""}), - "seed": ("INT", {"default": 0, "min": 0, "max": 9999999, "step": 1, "tooltip": "利用 seed 机制内置的自动步进器"}), + "seed": ("INT", {"default": 0, "min": 0, "max": 9999999, "step": 1, "tooltip": "Built-in auto-stepper using the seed mechanism"}), } } RETURN_TYPES = ("IMAGE", "MASK", "STRING", "STRING") RETURN_NAMES = ("IMAGE", "MASK", "FILE_NAME", "TOTAL_COUNT") FUNCTION = "load_images" - CATEGORY = "小白工具箱/批量处理" + CATEGORY = "XB_ToolBox/Batch_Process" def load_images(self, directory, seed): if not os.path.isdir(directory): - raise FileNotFoundError(f"未找到文件夹: {directory}") + raise FileNotFoundError(f"Directory not found: {directory}") valid_extensions = {'.png', '.jpg', '.jpeg', '.webp', '.bmp', '.jfif'} files = [f for f in os.listdir(directory) if os.path.splitext(f)[1].lower() in valid_extensions] @@ -38,23 +34,16 @@ class XB_BatchFolderLoader: files.sort(key=natural_sort_key) if not files: - raise Exception(f"文件夹内没有有效图片: {directory}") + raise Exception(f"No valid images found in directory: {directory}") total_count = len(files) current_file_idx = seed % total_count file_path = os.path.join(directory, files[current_file_idx]) - # 1. 提取原始文件名(不含后缀) raw_name = os.path.splitext(files[current_file_idx])[0] - - # 2. 获取真实的系统当天日期 (格式:2026-04-15) today_str = datetime.datetime.now().strftime("%Y-%m-%d") - - # 3. 注入纯净的路径前缀,彻底绕过 ComfyUI 的解析坑 - formatted_file_name = f"图片/批量处理/{today_str}/{raw_name}" - - # 4. 构造友好的数量提示字符串 - total_count_display = f"识别到图片总数为:{total_count}" + formatted_file_name = f"Images/Batch/{today_str}/{raw_name}" + total_count_display = f"Total images detected: {total_count}" img = Image.open(file_path) img = ImageOps.exif_transpose(img) diff --git a/nodes_blockswap.py b/nodes_blockswap.py index 2dec27c..c3e7f53 100644 --- a/nodes_blockswap.py +++ b/nodes_blockswap.py @@ -12,46 +12,40 @@ class AnyType(str): anyType = AnyType("*") def is_dynamic_vram_flag_used(): - """嗅探用户是否在启动参数中显式使用了 --enable-dynamic-vram""" return "--enable-dynamic-vram" in sys.argv - -# ============================================================================== -# 节点 1:UNet/DiT 纯净版切割 (严格还原第一版的纯物理转移) -# ============================================================================== class XB_UNetBlockSwap: @classmethod def INPUT_TYPES(s): return { "required": { - "unet_model": (anyType, {"tooltip": "仅接受纯净的 UNet/DiT 模型输入"}), + "unet_model": (anyType, {"tooltip": "Accepts pure UNet/DiT model input only"}), "blocks_to_swap": ("INT", { "default": 10, "min": 0, "max": 200, "step": 1, - "tooltip": "分配:将前 N 个核心块外包给系统内存。数值越大越省显存,但速度越慢。" + "tooltip": "Number of core blocks to offload to system RAM. Higher values save VRAM but slow down generation." }), }, } RETURN_TYPES = (anyType,) RETURN_NAMES = ("unet_model",) - CATEGORY = "小白工具箱/显存特攻" + CATEGORY = "XB_ToolBox/VRAM_Hacks" FUNCTION = "set_callback" def set_callback(self, unet_model: ModelPatcher, blocks_to_swap): if not isinstance(unet_model, ModelPatcher): return (unet_model,) - # 🚨 嗅探与休眠机制 (亮橙色) if is_dynamic_vram_flag_used(): - print("\n\033[93m[XB UNet块交换]\033[0m: ⚠️ 检测到参数 [--enable-dynamic-vram],分块节点自动休眠退让。") + print("\n\033[92m[XB UNet Block Swap]\033[0m: ⚠️ Detected parameter [--enable-dynamic-vram], block swap node auto-sleeping.") return (unet_model,) def swap_blocks(model_patcher: ModelPatcher, device_to, lowvram_model_memory, force_patch_weights, full_load): base_model = model_patcher.model - main_device = torch.device('cuda') # 严格还原第一版的设备指定 + main_device = torch.device('cuda') diffusion_model = getattr(base_model, 'diffusion_model', None) if not diffusion_model: @@ -70,11 +64,9 @@ class XB_UNetBlockSwap: if not all_blocks: return - # 正常分块亮蓝色提示 - print(f"\033[96m[XB UNet块交换]\033[0m: 静态物理分块已激活!锁定 {len(all_blocks)} 个引擎模块") + print(f"\033[96m[XB UNet Block Swap]\033[0m: Static physical block swap activated! Locked {len(all_blocks)} engine modules.") - # 🚀 大道至简:完全使用第一版的最原生的转移逻辑! - for b, block in tqdm(enumerate(all_blocks), total=len(all_blocks), desc="正在切割 UNet 流水线"): + for b, block in tqdm(enumerate(all_blocks), total=len(all_blocks), desc="Slicing UNet pipeline"): if b > blocks_to_swap: block.to(main_device) else: @@ -88,15 +80,12 @@ class XB_UNetBlockSwap: return (unet_model, ) -# ============================================================================== -# 节点 2:Checkpoint 混合包裹切割 (严格还原第一版的纯物理转移) -# ============================================================================== class XB_CheckpointBlockSwap: @classmethod def INPUT_TYPES(s): return { "required": { - "checkpoint_model": (anyType, {"tooltip": "接受任何模型输入 (含 CLIP/VAE 等完整架构)。"}), + "checkpoint_model": (anyType, {"tooltip": "Accepts any model input (including CLIP/VAE)."}), "blocks_to_swap": ("INT", {"default": 15, "min": 0, "max": 1000, "step": 1}), "offload_img_emb": ("BOOLEAN", {"default": True}), "offload_txt_emb": ("BOOLEAN", {"default": True}), @@ -106,16 +95,15 @@ class XB_CheckpointBlockSwap: RETURN_TYPES = (anyType,) RETURN_NAMES = ("checkpoint_model",) - CATEGORY = "小白工具箱/显存特攻" + CATEGORY = "XB_ToolBox/VRAM_Hacks" FUNCTION = "set_callback" def set_callback(self, checkpoint_model: ModelPatcher, blocks_to_swap, offload_txt_emb, offload_img_emb, use_non_blocking): if not isinstance(checkpoint_model, ModelPatcher): return (checkpoint_model,) - # 🚨 嗅探与休眠机制 (亮橙色) if is_dynamic_vram_flag_used(): - print("\n\033[93m[XB Checkpoint块交换]\033[0m: ⚠️ 检测到参数 [--enable-dynamic-vram],分块节点自动休眠退让。") + print("\n\033[92m[XB Checkpoint Block Swap]\033[0m: ⚠️ Detected parameter [--enable-dynamic-vram], block swap node auto-sleeping.") return (checkpoint_model,) def swap_blocks(model_patcher: ModelPatcher, device_to, lowvram_model_memory, force_patch_weights, full_load): @@ -137,15 +125,13 @@ class XB_CheckpointBlockSwap: break if all_blocks: - print(f"\033[96m[XB Checkpoint块交换]\033[0m: 静态物理分块已激活!锁定 {len(all_blocks)} 个引擎模块") - # 🚀 大道至简:完全使用第一版的最原生的转移逻辑! - for b, block in tqdm(enumerate(all_blocks), total=len(all_blocks), desc="正在切割 Checkpoint 流水线"): + print(f"\033[96m[XB Checkpoint Block Swap]\033[0m: Static physical block swap activated! Locked {len(all_blocks)} engine modules.") + 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) - # 原生方法处理 Embedding if offload_txt_emb: for path in ['text_embedding', 'caption_encoder', 'text_encoder']: if hasattr(diffusion_model, path) and getattr(diffusion_model, path) is not None: diff --git a/nodes_dashboard.py b/nodes_dashboard.py index 9e5c4e2..03379a0 100644 --- a/nodes_dashboard.py +++ b/nodes_dashboard.py @@ -1,10 +1,9 @@ class XB_Dashboard_Zen: - """ XB 远程控制中心 (终极版) """ @classmethod def INPUT_TYPES(s): return {"required": {}} RETURN_TYPES = () FUNCTION = "do_nothing" - CATEGORY = "小白工具箱/总控中心" + CATEGORY = "XB_ToolBox/Dashboard" def do_nothing(self, **kwargs): return () \ No newline at end of file diff --git a/nodes_pipeline.py b/nodes_pipeline.py index 2fa8f86..b478069 100644 --- a/nodes_pipeline.py +++ b/nodes_pipeline.py @@ -7,9 +7,6 @@ import comfy.samplers import folder_paths from .nodes_wan_vae import XB_WanFirstLastFrameToVideo -# ==================================================================== -# 积木一:【📦 视频参数打包总线】 -# ==================================================================== class XB_Wan_ParamBus: @classmethod def INPUT_TYPES(cls): @@ -19,14 +16,11 @@ class XB_Wan_ParamBus: "model_low": ("MODEL",), "clip": ("CLIP",), "vae": ("VAE",), - - "negative_prompt": ("STRING", {"multiline": True, "default": "色调艳丽,过曝,静态,细节模糊不清..."}), - + "negative_prompt": ("STRING", {"multiline": True, "default": "vivid colors, overexposed, static, blurry details..."}), "width": ("INT", {"forceInput": True}), "height": ("INT", {"forceInput": True}), "length": ("INT", {"forceInput": True}), "fps": ("FLOAT", {"forceInput": True}), - "vae_tile_size": ("INT", {"default": 64, "min": 64, "max": 3840, "step": 32}), "steps": ("INT", {"default": 4, "min": 1, "max": 100}), "high_noise_steps": ("INT", {"default": 2, "min": 1, "max": 50}), @@ -37,17 +31,13 @@ class XB_Wan_ParamBus: } RETURN_TYPES = ("WAN_BUS",) - RETURN_NAMES = ("📦 WAN_BUS (动态总线)",) + RETURN_NAMES = ("📦 WAN_BUS (Dynamic Bus)",) FUNCTION = "pack_bus" - CATEGORY = "小白工具箱/流水线架构" + CATEGORY = "XB_ToolBox/Pipeline" def pack_bus(self, **kwargs): return (kwargs,) - -# ==================================================================== -# 积木二:【🏃 首尾帧接力节点】 (🟢 回归 Required,重新召唤图片面板) -# ==================================================================== class XB_Wan_RelayNode: @classmethod def INPUT_TYPES(cls): @@ -55,20 +45,16 @@ class XB_Wan_RelayNode: files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))] if os.path.exists(input_dir) else [] files = sorted(files) - # 兜底:防止真没图的时候前端发神经 if not files: - files = ["[文件夹为空_请连线或上传]"] + files = ["[Folder empty_Please connect or upload]"] return { "required": { "wan_bus": ("WAN_BUS",), "start_image": ("IMAGE",), - - "positive_prompt": ("STRING", {"multiline": True, "default": "描述这一段的具体动作..."}), + "positive_prompt": ("STRING", {"multiline": True, "default": "Describe the specific action for this segment..."}), "seed": ("INT", {"default": 123456789}), - "cut_first_frame": ("BOOLEAN", {"default": True, "label_on": "是 (接力去重)", "label_off": "否 (保留首帧)"}), - - # 🟢 核心修复:必须放在 required 里,面板才会重新出现! + "cut_first_frame": ("BOOLEAN", {"default": True, "label_on": "Yes (Relay Deduplication)", "label_off": "No (Keep First Frame)"}), "end_image_file": (files, {"image_upload": True}) }, "optional": { @@ -78,31 +64,31 @@ class XB_Wan_RelayNode: } RETURN_TYPES = ("WAN_BUS", "IMAGE", "IMAGE") - RETURN_NAMES = ("📦 WAN_BUS (传给下段)", "🖼️ 当前尾图 (接下段首图)", "🎞️ 累加视频流 (接下段或输出)") + RETURN_NAMES = ("📦 WAN_BUS (Pass to next)", "🖼️ Current End Image (Connect to next Start Image)", "🎞️ Accumulated Video Stream") FUNCTION = "execute_relay" - CATEGORY = "小白工具箱/流水线架构" + CATEGORY = "XB_ToolBox/Pipeline" def execute_relay(self, wan_bus, start_image, positive_prompt, seed, cut_first_frame, end_image_file, opt_end_image=None, prev_video=None): - print("\n🏃 [XB-BOX] 正在执行单批次首尾帧生成任务...") + print("\n🏃 [XB-BOX] Executing single-batch first/last frame generation task...") b = wan_bus if opt_end_image is not None: end_image = opt_end_image - print(f"🖼️ [XB-BOX] 检测到连线,正在优先使用 opt_end_image 端点传入的尾帧。") + print(f"🖼️ [XB-BOX] Connection detected, prioritizing end frame from opt_end_image endpoint.") else: - if end_image_file == "[文件夹为空_请连线或上传]": - raise ValueError("🚨 [XB-BOX] 尾帧缺失!请使用左侧 opt_end_image 连入图片,或点击面板上传图片!") + if end_image_file == "[Folder empty_Please connect or upload]": + raise ValueError("🚨 [XB-BOX] End frame missing! Connect image to opt_end_image or upload via panel!") image_path = folder_paths.get_annotated_filepath(end_image_file) if not os.path.exists(image_path): - raise ValueError(f"🚨 [XB-BOX] 找不到图片文件: {image_path}") + raise ValueError(f"🚨 [XB-BOX] Image file not found: {image_path}") i = Image.open(image_path) i = ImageOps.exif_transpose(i) image_data = i.convert("RGB") image_data = np.array(image_data).astype(np.float32) / 255.0 end_image = torch.from_numpy(image_data)[None,] - print(f"🖼️ [XB-BOX] 已成功加载面板预览图: {end_image_file}") + print(f"🖼️ [XB-BOX] Successfully loaded panel preview image: {end_image_file}") pos_cond, = nodes.CLIPTextEncode().encode(b["clip"], positive_prompt) neg_cond, = nodes.CLIPTextEncode().encode(b["clip"], b["negative_prompt"]) @@ -115,7 +101,7 @@ class XB_Wan_RelayNode: batch_size=1, vae_tile_size=b["vae_tile_size"] ) - print(f"🔥 [XB-BOX] 启动高噪引擎 (0 -> {b['high_noise_steps']} 步)...") + print(f"🔥 [XB-BOX] Starting high noise engine (0 -> {b['high_noise_steps']} steps)...") latent_high, = nodes.KSamplerAdvanced().sample( model=b["model_high"], add_noise="enable", noise_seed=seed, steps=b["steps"], cfg=b["cfg"], sampler_name=b["sampler_name"], scheduler=b["scheduler"], @@ -123,7 +109,7 @@ class XB_Wan_RelayNode: start_at_step=0, end_at_step=b["high_noise_steps"], return_with_leftover_noise="enable" ) - print(f"❄️ [XB-BOX] 切换低噪引擎 ({b['high_noise_steps']} -> 补齐满步)...") + print(f"❄️ [XB-BOX] Switching to low noise engine ({b['high_noise_steps']} -> completion)...") latent_low, = nodes.KSamplerAdvanced().sample( model=b["model_low"], add_noise="disable", noise_seed=seed, steps=b["steps"], cfg=b["cfg"], sampler_name=b["sampler_name"], scheduler=b["scheduler"], @@ -143,7 +129,7 @@ class XB_Wan_RelayNode: decoded_image = decoded_image[1:, :, :, :] else: decoded_image = decoded_image[:, 1:, :, :, :] - print("✂️ [XB-BOX] 已触发物理去重:切除重叠首帧。") + print("✂️ [XB-BOX] Physical deduplication triggered: overlap first frame removed.") if is_4d: last_frame = decoded_image[-1:, :, :, :] @@ -152,22 +138,18 @@ class XB_Wan_RelayNode: last_frame = decoded_image[:, -1:, :, :, :] out_length = decoded_image.shape[1] - print(f"✅ [XB-BOX] 当前批次完成!新生成片段长度: {out_length} 帧") + print(f"✅ [XB-BOX] Current batch complete! New segment length: {out_length} frames") if prev_video is not None: - print("🧩 [XB-BOX] 检测到连入的上一段视频,正在执行雪球累加合并...") + print("🧩 [XB-BOX] Previous video connection detected, executing snowball accumulation merge...") concat_dim = 0 if is_4d else 1 final_video = torch.cat([prev_video, decoded_image], dim=concat_dim) - print(f"🎉 [XB-BOX] 雪球累加完成!当前长视频已达: {final_video.shape[concat_dim]} 帧") + print(f"🎉 [XB-BOX] Snowball accumulation complete! Current long video reached: {final_video.shape[concat_dim]} frames") else: final_video = decoded_image return (wan_bus, last_frame, final_video) - -# ==================================================================== -# 积木三:【🧩 视频无缝拼接器】 -# ==================================================================== class XB_Video_Merger: @classmethod def INPUT_TYPES(cls): @@ -177,9 +159,9 @@ class XB_Video_Merger: return inputs RETURN_TYPES = ("IMAGE",) - RETURN_NAMES = ("🎞️ 完整长视频",) + RETURN_NAMES = ("🎞️ Full Long Video",) FUNCTION = "merge_videos" - CATEGORY = "小白工具箱/流水线架构" + CATEGORY = "XB_ToolBox/Pipeline" def merge_videos(self, **kwargs): videos = [] @@ -189,22 +171,18 @@ class XB_Video_Merger: videos.append(vid) if not videos: - raise ValueError("🚨 [XB-BOX] 合并失败:至少需要连入一段视频!") + raise ValueError("🚨 [XB-BOX] Merge failed: At least one video must be connected!") - print(f"🧩 [XB-BOX] 正在执行 {len(videos)} 段视频无缝物理合并...") + print(f"🧩 [XB-BOX] Executing seamless physical merge of {len(videos)} video segments...") concat_dim = 0 if len(videos[0].shape) == 4 else 1 final_video = torch.cat(videos, dim=concat_dim) total_frames = final_video.shape[concat_dim] - print(f"🎉 [XB-BOX] 视频合并成功!长视频总帧数: {total_frames} 帧") + print(f"🎉 [XB-BOX] Video merge successful! Total frames: {total_frames}") return (final_video,) - -# ==================================================================== -# 积木四:【🧊 分镜阵列切割机】 -# ==================================================================== class XB_StoryboardSlicer: @classmethod def INPUT_TYPES(cls): @@ -212,31 +190,31 @@ class XB_StoryboardSlicer: "required": { "image": ("IMAGE",), "mode": ([ - "三宫格 (1行3列 · 横向)", - "三宫格 (3行1列 · 竖向)", - "四宫格 (2行2列)", - "六宫格 (2行3列 · 横向)", - "六宫格 (3行2列 · 竖向)", - "九宫格 (3行3列)" - ], {"default": "四宫格 (2行2列)"}), + "3-Grid (1x3 Horizontal)", + "3-Grid (3x1 Vertical)", + "4-Grid (2x2)", + "6-Grid (2x3 Horizontal)", + "6-Grid (3x2 Vertical)", + "9-Grid (3x3)" + ], {"default": "4-Grid (2x2)"}), "crop_margin": ("FLOAT", {"default": 0.00, "min": 0.00, "max": 0.45, "step": 0.01}), } } RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE") - RETURN_NAMES = ("图1", "图2", "图3", "图4", "图5", "图6", "图7", "图8", "图9") + RETURN_NAMES = ("Img1", "Img2", "Img3", "Img4", "Img5", "Img6", "Img7", "Img8", "Img9") FUNCTION = "slice_image" - CATEGORY = "小白工具箱/流水线架构" + CATEGORY = "XB_ToolBox/Pipeline" def slice_image(self, image, mode, crop_margin): - print(f"\n✂️ [XB-BOX] 启动分镜切割机,当前模式: {mode} | 裁边比例: {crop_margin}") + print(f"\n✂️ [XB-BOX] Starting storyboard slicer, mode: {mode} | crop margin: {crop_margin}") - if "1行3列" in mode: rows, cols = 1, 3 - elif "3行1列" in mode: rows, cols = 3, 1 - elif "2行2列" in mode: rows, cols = 2, 2 - elif "2行3列" in mode: rows, cols = 2, 3 - elif "3行2列" in mode: rows, cols = 3, 2 - elif "3行3列" in mode: rows, cols = 3, 3 + if "1x3" in mode: rows, cols = 1, 3 + elif "3x1" in mode: rows, cols = 3, 1 + elif "2x2" in mode: rows, cols = 2, 2 + elif "2x3" in mode: rows, cols = 2, 3 + elif "3x2" in mode: rows, cols = 3, 2 + elif "3x3" in mode: rows, cols = 3, 3 else: rows, cols = 2, 2 batch_size, h, w, channels = image.shape @@ -247,7 +225,7 @@ class XB_StoryboardSlicer: crop_y = int(slice_h * crop_margin) crop_x = int(slice_w * crop_margin) - print(f"📐 [XB-BOX] 原图: {w}x{h} -> 原始切片: {slice_w}x{slice_h} -> 环切后实际输出: {slice_w - crop_x*2}x{slice_h - crop_y*2}") + print(f"📐 [XB-BOX] Original: {w}x{h} -> Raw slice: {slice_w}x{slice_h} -> Final output after crop: {slice_w - crop_x*2}x{slice_h - crop_y*2}") sliced_images = [] @@ -271,6 +249,6 @@ class XB_StoryboardSlicer: sliced_images.append(black_placeholder) empty_count += 1 - print(f"✅ [XB-BOX] 分割完成: {rows * cols} 张有效画面 | 🛡️ 已生成 {empty_count} 个纯黑防爆占位符。") + print(f"✅ [XB-BOX] Slicing complete: {rows * cols} valid images | 🛡️ Generated {empty_count} black placeholder(s).") return tuple(sliced_images) \ No newline at end of file diff --git a/nodes_sageatt.py b/nodes_sageatt.py new file mode 100644 index 0000000..2de9faf --- /dev/null +++ b/nodes_sageatt.py @@ -0,0 +1,81 @@ +import comfy.model_patcher +import torch + +class XB_SageAttentionAccelerator: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model": ("MODEL",), + "preset": ([ + "Mode A (128x128x32)", + "Mode B (128x64x96)", + "Mode C (128x16x16)", + "Mode D (64x64x16)", + ], {"default": "Mode C (128x16x16)"}), + } + } + + RETURN_TYPES = ("MODEL",) + FUNCTION = "patch" + CATEGORY = "XB_ToolBox/VRAM_Hacks" + + def patch(self, model, preset): + configs = { + "Mode A (128x128x32)": {'M': 128, 'N': 128, 'GROUP': 32, 'WAVE': 2, 'WARP': 8, 'NSTAGES': 1}, + "Mode B (128x64x96)": {'M': 128, 'N': 64, 'GROUP': 96, 'WAVE': 3, 'WARP': 8, 'NSTAGES': 2}, + "Mode C (128x16x16)": {'M': 128, 'N': 16, 'GROUP': 16, 'WAVE': 2, 'WARP': 4, 'NSTAGES': 2}, + "Mode D (64x64x16)": {'M': 64, 'N': 64, 'GROUP': 16, 'WAVE': 4, 'WARP': 4, 'NSTAGES': 2}, + } + + selected_cfg = configs[preset] + + def attention_override_sage(func, q, k, v, heads, mask=None, attn_precision=None, skip_reshape=False, skip_output_reshape=False, **kwargs): + try: + from sageattention import sageattn + except ImportError: + raise Exception("🚨 Error: sageattention module not found. Please verify installation.") + + in_dtype = v.dtype + + if q.dtype == torch.float32 or k.dtype == torch.float32 or v.dtype == torch.float32: + q, k, v = q.to(torch.float16), k.to(torch.float16), v.to(torch.float16) + + if skip_reshape: + b, _, _, dim_head = q.shape + tensor_layout = "HND" + else: + b, _, dim_head = q.shape + dim_head //= heads + q = q.view(b, -1, heads, dim_head) + k = k.view(b, -1, heads, dim_head) + v = v.view(b, -1, heads, dim_head) + tensor_layout = "NHD" + + if mask is not None: + if mask.ndim == 2: mask = mask.unsqueeze(0) + if mask.ndim == 3: mask = mask.unsqueeze(1) + + out = sageattn(q, k, v, is_causal=False, tensor_layout=tensor_layout, sage_config=selected_cfg).to(in_dtype) + + if tensor_layout == "HND": + if not skip_output_reshape: + out = out.transpose(1, 2).reshape(b, -1, heads * dim_head) + else: + if skip_output_reshape: + out = out.transpose(1, 2) + else: + out = out.reshape(b, -1, heads * dim_head) + + return out + + m = model.clone() + + if "transformer_options" not in m.model_options: + m.model_options["transformer_options"] = {} + + m.model_options["transformer_options"]["optimized_attention_override"] = attention_override_sage + + print(f"\033[96m[XB-BOX]\033[0m: SageAttention engine switched to \033[92m{preset}\033[0m") + + return (m,) \ No newline at end of file diff --git a/nodes_tile.py b/nodes_tile.py index 1b28781..b6a0365 100644 --- a/nodes_tile.py +++ b/nodes_tile.py @@ -9,7 +9,7 @@ class XB_SamplerChunkMaster: return { "required": { "model": ("MODEL",), - "stat_mode": (["完整预览", "仅显示分块"], {"default": "完整预览"}), + "stat_mode": (["Full Preview", "Chunks Only"], {"default": "Full Preview"}), "tile_size": ("INT", {"default": 448, "min": 128, "max": 1024, "step": 32}), "tile_overlap": ("INT", {"default": 32, "min": 0, "max": 128, "step": 8}), "frame_chunk_size": ("INT", {"default": 17, "min": 1, "max": 129, "step": 8}), @@ -24,28 +24,24 @@ class XB_SamplerChunkMaster: RETURN_TYPES = ("MODEL", "IMAGE") RETURN_NAMES = ("model", "preview_image") FUNCTION = "apply_and_preview" - CATEGORY = "小白工具箱/分块工具" + CATEGORY = "XB_ToolBox/Tile_Tools" def apply_and_preview(self, model, stat_mode, tile_size, tile_overlap, frame_chunk_size, frame_chunk_overlap, rocm_optimized, latent_info=None): - # 🚨 架构级修复:绝对禁止使用 clone()!直接原地暴力注入,确保底层 C++/HIP 钩子能抓取到内存地址! model.model_options["wan_tile_size"] = tile_size model.model_options["wan_tile_overlap"] = tile_overlap model.model_options["wan_frame_chunk"] = frame_chunk_size - model_copy = model # 返回原对象引用 + model_copy = model - # 针对 ROCm 的激进策略适配 if rocm_optimized: model.model_options["wan_frame_overlap"] = frame_chunk_overlap model.model_options["rocm_optimized"] = True - # 🧹 执行分块注入前,强制进行一次深度垃圾回收,打破 PyTorch 显存缓存池假象 gc.collect() if torch.cuda.is_available(): torch.cuda.empty_cache() - if hasattr(torch.cuda, "ipc_collect"): # 安全防护 + if hasattr(torch.cuda, "ipc_collect"): torch.cuda.ipc_collect() - # 📊 自动推算显示参数 width = 1280 height = 720 total_frames = 81 @@ -59,7 +55,6 @@ class XB_SamplerChunkMaster: height = samples.shape[2] * 8 width = samples.shape[3] * 8 - # 🎨 绘制高级可视化面板 preview_img = self.draw_complex_preview( stat_mode, width, height, total_frames, tile_size, tile_overlap, diff --git a/nodes_video.py b/nodes_video.py index f77f565..be39e87 100644 --- a/nodes_video.py +++ b/nodes_video.py @@ -3,7 +3,7 @@ class XB_ImageParamsMaster: def INPUT_TYPES(cls): return { "required": { - "aspect_ratio": (["自由 (Free)", "1:1", "16:9", "9:16", "4:3", "3:4", "21:9"], {"default": "自由 (Free)"}), + "aspect_ratio": (["Free", "1:1", "16:9", "9:16", "4:3", "3:4", "21:9"], {"default": "Free"}), "width": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 1}), "height": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 1}), "batch_size": ("INT", {"default": 1, "min": 1, "max": 1000, "step": 1}), @@ -13,12 +13,12 @@ class XB_ImageParamsMaster: } RETURN_TYPES = ("INT", "INT", "INT", "FLOAT", "INT", "INT") - RETURN_NAMES = ("图片宽度", "图片高度", "生成数量", "浮点控制", "整数控制", "缩放尺寸") + RETURN_NAMES = ("Image Width", "Image Height", "Batch Size", "Float Control", "Int Control", "Scale Size") FUNCTION = "process" - CATEGORY = "小白工具箱/图像参数" + CATEGORY = "XB_ToolBox/Image_Params" def process(self, aspect_ratio, width, height, batch_size, strength_float, strength_int): - if "自由" in aspect_ratio: + if "Free" in aspect_ratio: return (width, height, batch_size, float(strength_float), int(strength_int), max(width, height)) step = 16 @@ -27,14 +27,13 @@ class XB_ImageParamsMaster: return (safe_w, safe_h, batch_size, float(strength_float), int(strength_int), max(safe_w, safe_h)) - class XB_VideoParamsMaster: @classmethod def INPUT_TYPES(cls): return { "required": { - "aspect_ratio": (["自由 (Free)", "1:1", "16:9", "9:16", "4:3", "3:4", "21:9"], {"default": "自由 (Free)"}), - "duration_display": ("STRING", {"default": "视频时长: 0.00 秒", "multiline": False}), + "aspect_ratio": (["Free", "1:1", "16:9", "9:16", "4:3", "3:4", "21:9"], {"default": "Free"}), + "duration_display": ("STRING", {"default": "Video Duration: 0.00 s", "multiline": False}), "width": ("INT", {"default": 480, "min": 64, "max": 8192, "step": 1}), "height": ("INT", {"default": 832, "min": 64, "max": 8192, "step": 1}), "length": ("INT", {"default": 81, "min": 1, "max": 9999, "step": 1}), @@ -44,12 +43,12 @@ class XB_VideoParamsMaster: } RETURN_TYPES = ("INT", "INT", "INT", "INT", "FLOAT", "INT") - RETURN_NAMES = ("宽度", "高度", "帧数", "帧率", "帧率_浮点", "缩放尺寸") + RETURN_NAMES = ("Width", "Height", "Frames", "FPS", "FPS_Float", "Scale Size") FUNCTION = "process" - CATEGORY = "小白工具箱/图像参数" + CATEGORY = "XB_ToolBox/Image_Params" def process(self, aspect_ratio, duration_display, width, height, length, fps, fps_float): - if "自由" in aspect_ratio: + if "Free" in aspect_ratio: return (width, height, length, int(round(fps)), float(fps), max(width, height)) golden_buckets = { @@ -71,37 +70,32 @@ class XB_VideoParamsMaster: return (safe_w, safe_h, safe_len, final_fps, float(final_fps), max(safe_w, safe_h)) - -# ========================================== -# 🎛️ 新增:全能参数控制节点 -# ========================================== class XB_MasterParameter: @classmethod def INPUT_TYPES(cls): return { "required": { - "mode": (["自由模式", "模型模式", "编码模式", "解码模式", "比例模式", "其他模式"], {"default": "自由模式"}), - # 🟢 恢复最纯正的 0-9999 范围,默认步长 0.01 + "mode": (["Free Mode", "Model Mode", "Encode Mode", "Decode Mode", "Ratio Mode", "Other Mode"], {"default": "Free Mode"}), "value": ("FLOAT", {"default": 1024.0, "min": 0.0, "max": 9999.0, "step": 0.01}), } } RETURN_TYPES = ("INT", "FLOAT") - RETURN_NAMES = ("整数 (INT)", "浮点 (FLOAT)") + RETURN_NAMES = ("Integer (INT)", "Float (FLOAT)") FUNCTION = "get_value" - CATEGORY = "小白工具箱/参数控制" + CATEGORY = "XB_ToolBox/Params_Control" def get_value(self, mode, value): - if mode == "自由模式": + if mode == "Free Mode": val = max(0.0, min(9999.0, value)) - elif mode == "模型模式": + elif mode == "Model Mode": val = max(0.0, min(50.0, value)) - elif mode in ["编码模式", "解码模式"]: + elif mode in ["Encode Mode", "Decode Mode"]: val = max(64.0, min(3840.0, value)) val = round(val / 32) * 32 - elif mode == "比例模式": + elif mode == "Ratio Mode": val = max(0.0, min(1.0, value)) - elif mode == "其他模式": + elif mode == "Other Mode": val = max(0.0, min(9999.0, value)) else: val = value diff --git a/nodes_vis.py b/nodes_vis.py index 6f206c0..761fde6 100644 --- a/nodes_vis.py +++ b/nodes_vis.py @@ -3,15 +3,9 @@ import numpy as np import os from PIL import Image, ImageDraw, ImageFont -# ========================================== -# 极客魔法:标识数据来源,并携带分层数据的超级浮点数 -# ========================================== class CalculatedFloat(float): pass -# ========================================== -# 📟 可用显存计算 -# ========================================== class XB_VRAM_Calculator: def __init__(self): pass @@ -20,22 +14,22 @@ class XB_VRAM_Calculator: def INPUT_TYPES(cls): return { "required": { - "显卡总显存_GB": ("INT", {"default": 24, "min": 4, "max": 128, "step": 1, "display": "number"}), - "系统及背景开销_GB": ("FLOAT", {"default": 2.0, "min": 0.0, "max": 64.0, "step": 0.5, "display": "number"}), - "视频主模型": (["LTX-2.3 (22B)", "WAN2.2-I2V (14B)", "WAN2.2-T2V (14B)", "WAN2.2-Animate (14B)"], {"default": "LTX-2.3 (22B)"}), - "模型量化": (["FP16/BF16", "FP8", "GGUF-Q8", "GGUF-Q6", "GGUF-Q4"], {"default": "FP8"}), - "主模型总层数": ("INT", {"default": 48, "min": 1, "max": 200, "step": 1, "display": "number"}), - "分层交换数值": ("INT", {"default": 0, "min": 0, "max": 200, "step": 1, "display": "number"}), - "LoRA及插件总计_GB": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 64.0, "step": 0.1, "display": "number"}), + "Total_VRAM_GB": ("INT", {"default": 24, "min": 4, "max": 128, "step": 1, "display": "number"}), + "System_Overhead_GB": ("FLOAT", {"default": 2.0, "min": 0.0, "max": 64.0, "step": 0.5, "display": "number"}), + "Main_Video_Model": (["LTX-2.3 (22B)", "WAN2.2-I2V (14B)", "WAN2.2-T2V (14B)", "WAN2.2-Animate (14B)"], {"default": "LTX-2.3 (22B)"}), + "Model_Quantization": (["FP16/BF16", "FP8", "GGUF-Q8", "GGUF-Q6", "GGUF-Q4"], {"default": "FP8"}), + "Total_Model_Layers": ("INT", {"default": 48, "min": 1, "max": 200, "step": 1, "display": "number"}), + "Layers_to_Swap": ("INT", {"default": 0, "min": 0, "max": 200, "step": 1, "display": "number"}), + "LoRA_Plugin_VRAM_GB": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 64.0, "step": 0.1, "display": "number"}), } } RETURN_TYPES = ("FLOAT",) - RETURN_NAMES = ("可用显存输出",) + RETURN_NAMES = ("Available_VRAM",) FUNCTION = "calculate_vram" - CATEGORY = "小白工具箱/分块预览" + CATEGORY = "XB_ToolBox/Chunk_Preview" - def calculate_vram(self, 显卡总显存_GB, 系统及背景开销_GB, 视频主模型, 模型量化, 主模型总层数, 分层交换数值, LoRA及插件总计_GB): + def calculate_vram(self, Total_VRAM_GB, System_Overhead_GB, Main_Video_Model, Model_Quantization, Total_Model_Layers, Layers_to_Swap, LoRA_Plugin_VRAM_GB): vram_map = { "LTX-2.3 (22B)": {"FP16/BF16": 44.0, "FP8": 22.5, "GGUF-Q8": 22.8, "GGUF-Q6": 17.8, "GGUF-Q4": 13.5}, "WAN2.2-I2V (14B)": {"FP16/BF16": 28.0, "FP8": 14.0, "GGUF-Q8": 14.5, "GGUF-Q6": 11.0, "GGUF-Q4": 9.0}, @@ -43,21 +37,18 @@ class XB_VRAM_Calculator: "WAN2.2-Animate (14B)": {"FP16/BF16": 28.0, "FP8": 14.0, "GGUF-Q8": 14.5, "GGUF-Q6": 11.0, "GGUF-Q4": 9.0}, } - full_model_size = vram_map.get(视频主模型, {}).get(模型量化, 14.0) - stay_layers = max(0, 主模型总层数 - 分层交换数值) - active_model_vram = (full_model_size / max(1, 主模型总层数)) * stay_layers - available_vram = 显卡总显存_GB - 系统及背景开销_GB - active_model_vram - LoRA及插件总计_GB + full_model_size = vram_map.get(Main_Video_Model, {}).get(Model_Quantization, 14.0) + stay_layers = max(0, Total_Model_Layers - Layers_to_Swap) + active_model_vram = (full_model_size / max(1, Total_Model_Layers)) * stay_layers + available_vram = Total_VRAM_GB - System_Overhead_GB - active_model_vram - LoRA_Plugin_VRAM_GB available_vram = max(0.1, available_vram) res = CalculatedFloat(available_vram) res._stay = stay_layers - res._total = 主模型总层数 + res._total = Total_Model_Layers return (res,) -# ========================================== -# 🧊 极简双区浏览器:左 2D 原生镂空 + 右完美重叠圆柱 -# ========================================== class XB_ChunkVisualization: def __init__(self): pass @@ -66,26 +57,26 @@ class XB_ChunkVisualization: def INPUT_TYPES(cls): return { "required": { - "统计模式": (["仅画面", "仅时间", "画面与时间"], {"default": "画面与时间"}), - "可用显存_GB": ("FLOAT", {"default": 12.0, "min": 0.1, "max": 128.0, "step": 0.1, "display": "number"}), - "图像宽度": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 16, "display": "number"}), - "图像高度": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 16, "display": "number"}), - "图像帧数": ("INT", {"default": 1, "min": 1, "max": 10000, "step": 4, "display": "number"}), - "当前阶段": (["编码阶段", "解码阶段"], {"default": "解码阶段"}), - "图像分块大小": ("INT", {"default": 512, "min": 64, "max": 8192, "step": 32, "display": "number"}), - "图像分块重叠": ("INT", {"default": 64, "min": 0, "max": 4096, "step": 32, "display": "number"}), - "帧数分段大小": ("INT", {"default": 33, "min": 8, "max": 10000, "step": 4, "display": "number"}), - "帧数分段重叠": ("INT", {"default": 4, "min": 0, "max": 1000, "step": 4, "display": "number"}), + "Stat_Mode": (["Spatial Only", "Temporal Only", "Spatial & Temporal"], {"default": "Spatial & Temporal"}), + "Available_VRAM_GB": ("FLOAT", {"default": 12.0, "min": 0.1, "max": 128.0, "step": 0.1, "display": "number"}), + "Image_Width": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 16, "display": "number"}), + "Image_Height": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 16, "display": "number"}), + "Image_Frames": ("INT", {"default": 1, "min": 1, "max": 10000, "step": 4, "display": "number"}), + "Current_Stage": (["Encode Stage", "Decode Stage"], {"default": "Decode Stage"}), + "Spatial_Tile_Size": ("INT", {"default": 512, "min": 64, "max": 8192, "step": 32, "display": "number"}), + "Spatial_Tile_Overlap": ("INT", {"default": 64, "min": 0, "max": 4096, "step": 32, "display": "number"}), + "Temporal_Chunk_Size": ("INT", {"default": 33, "min": 8, "max": 10000, "step": 4, "display": "number"}), + "Temporal_Chunk_Overlap": ("INT", {"default": 4, "min": 0, "max": 1000, "step": 4, "display": "number"}), }, "optional": { - "图像输入": ("IMAGE",) + "Image_Input": ("IMAGE",) } } RETURN_TYPES = ("IMAGE",) RETURN_NAMES = ("preview_image",) FUNCTION = "visualize_chunks" - CATEGORY = "小白工具箱" + CATEGORY = "XB_ToolBox" def get_1d_chunks(self, total_len, chunk_size, overlap): chunks = [] @@ -99,28 +90,28 @@ class XB_ChunkVisualization: curr += stride return chunks - def visualize_chunks(self, 统计模式, 可用显存_GB, 图像宽度, 图像高度, 图像帧数, 当前阶段, - 图像分块大小, 图像分块重叠, 帧数分段大小, 帧数分段重叠, 图像输入=None): + def visualize_chunks(self, Stat_Mode, Available_VRAM_GB, Image_Width, Image_Height, Image_Frames, Current_Stage, + Spatial_Tile_Size, Spatial_Tile_Overlap, Temporal_Chunk_Size, Temporal_Chunk_Overlap, Image_Input=None): - active_vram = float(可用显存_GB) - is_calc = type(可用显存_GB).__name__ == "CalculatedFloat" - stay_info = f" (模型驻留:{可用显存_GB._stay}/{可用显存_GB._total}层)" if (is_calc and hasattr(可用显存_GB, '_stay')) else "" + active_vram = float(Available_VRAM_GB) + is_calc = type(Available_VRAM_GB).__name__ == "CalculatedFloat" + stay_info = f" (Reside:{Available_VRAM_GB._stay}/{Available_VRAM_GB._total}L)" if (is_calc and hasattr(Available_VRAM_GB, '_stay')) else "" - if 图像输入 is not None: - B, H_orig, W_orig, C = 图像输入.shape - source_img = Image.fromarray((图像输入[0].cpu().numpy() * 255).astype(np.uint8)).convert("RGBA") - res_info = f"【图像尺寸】: {W_orig}x{H_orig} (来自图像输入)" + if Image_Input is not None: + B, H_orig, W_orig, C = Image_Input.shape + source_img = Image.fromarray((Image_Input[0].cpu().numpy() * 255).astype(np.uint8)).convert("RGBA") + res_info = f"[Size]: {W_orig}x{H_orig} (Input)" else: - W_orig, H_orig = 图像宽度, 图像高度 + W_orig, H_orig = Image_Width, Image_Height source_img = Image.new("RGBA", (W_orig, H_orig), (25, 25, 25, 255)) - res_info = f"【图像尺寸】: {W_orig}x{H_orig} (来自手动参数)" + res_info = f"[Size]: {W_orig}x{H_orig} (Param)" - x_chunks = self.get_1d_chunks(W_orig, 图像分块大小 if "画面" in 统计模式 else 0, 图像分块重叠) - y_chunks = self.get_1d_chunks(H_orig, 图像分块大小 if "画面" in 统计模式 else 0, 图像分块重叠) - t_chunks = self.get_1d_chunks(图像帧数, 帧数分段大小 if "时间" in 统计模式 else 0, 帧数分段重叠) + x_chunks = self.get_1d_chunks(W_orig, Spatial_Tile_Size if "Spatial" in Stat_Mode else 0, Spatial_Tile_Overlap) + y_chunks = self.get_1d_chunks(H_orig, Spatial_Tile_Size if "Spatial" in Stat_Mode else 0, Spatial_Tile_Overlap) + t_chunks = self.get_1d_chunks(Image_Frames, Temporal_Chunk_Size if "Temporal" in Stat_Mode else 0, Temporal_Chunk_Overlap) total_batches = len(x_chunks) * len(y_chunks) * len(t_chunks) - base_vol = W_orig * H_orig * 图像帧数 + base_vol = W_orig * H_orig * Image_Frames real_vol, max_chunk_vol = 0, 0 for x1, x2 in x_chunks: for y1, y2 in y_chunks: @@ -141,35 +132,31 @@ class XB_ChunkVisualization: return ImageFont.truetype(p, size) if os.path.exists(p) else ImageFont.load_default() f_info = get_font(22) - f_num = get_font(28) # 大号字体用于分区大标题和块标号 + f_num = get_font(28) temp_draw = ImageDraw.Draw(Image.new("RGBA", (1,1))) - # ========================================== - # 📐 独立区域布局计算 - # ========================================== - show_time = ("时间" in 统计模式 and 图像帧数 > 1) + show_time = ("Temporal" in Stat_Mode and Image_Frames > 1) sidebar_w = 250 if show_time else 0 gap = 50 if show_time else 0 - padding = 70 # 增加顶部 padding 以容纳大标题 + padding = 70 draw_W = W_orig + gap + sidebar_w - new_W = max(draw_W + 80, 700) # 左右各留 40 + new_W = max(draw_W + 80, 700) max_txt_w = new_W - 50 - # 数据面板排版 info_cats = [ - {"text": f"【统计模式】: {统计模式} / 【当前阶段】: {当前阶段}", "color": (0, 255, 255)}, + {"text": f"[Mode]: {Stat_Mode} / [Stage]: {Current_Stage}", "color": (0, 255, 255)}, {"text": res_info, "color": (220, 220, 220)}, - {"text": f"【加载批次】: {total_batches} 批 (图像:{len(x_chunks)*len(y_chunks)} × 时间:{len(t_chunks)})", "color": (255, 255, 255)}, - {"text": f"【可用显存】: {active_vram:.1f}G{stay_info} ({'计算参数' if is_calc else '手输参数'})", "color": (150, 200, 255)}, - {"text": f"【总数据量】: {real_vol/1e6:.1f}M (基准:{base_vol/1e6:.1f}M | 冗余:+{redundancy:.1f}%) [注:M为百万像素点]", "color": (255, 150, 200)}, - {"text": f"【单批体积】: {max_chunk_vol/1e6:.1f}M / 批 / 【显存压力】: {kappa:.2f}", "color": p_color} + {"text": f"[Batches]: {total_batches} (S:{len(x_chunks)*len(y_chunks)} x T:{len(t_chunks)})", "color": (255, 255, 255)}, + {"text": f"[VRAM]: {active_vram:.1f}G{stay_info}", "color": (150, 200, 255)}, + {"text": f"[Volume]: {real_vol/1e6:.1f}M (Base:{base_vol/1e6:.1f}M | Redundancy:+{redundancy:.1f}%)", "color": (255, 150, 200)}, + {"text": f"[Chunk]: {max_chunk_vol/1e6:.1f}M/b / [Pressure]: {kappa:.2f}", "color": p_color} ] final_lines = [] for cat in info_cats: - prefix, content = cat["text"].split("】: ", 1) if "】: " in cat["text"] else ("", cat["text"]) - if prefix: prefix += "】: " + prefix, content = cat["text"].split("]: ", 1) if "]: " in cat["text"] else ("", cat["text"]) + if prefix: prefix += "]: " prefix_w = int(temp_draw.textlength(prefix, font=f_info)) curr, is_first = "", True for char in content: @@ -185,13 +172,12 @@ class XB_ChunkVisualization: line_h = 36 header_H = len(final_lines) * line_h + 80 - # 画布定位 paste_x = int((new_W - draw_W) / 2) paste_y = header_H + padding full_canvas = Image.new("RGBA", (int(new_W), int(paste_y + H_orig + 40)), (20, 20, 20, 255)) - if 统计模式 != "仅时间": + if Stat_Mode != "Temporal Only": full_canvas.paste(source_img, (paste_x, paste_y)) else: faded = source_img.copy() @@ -200,7 +186,6 @@ class XB_ChunkVisualization: draw = ImageDraw.Draw(full_canvas, "RGBA") - # 数据区文本 y = 15 for txt, clr, x_off in final_lines: draw.text((25 + x_off, y), txt, fill=clr, font=f_info) @@ -211,23 +196,18 @@ class XB_ChunkVisualization: colors = [(0, 255, 0), (0, 255, 255), (255, 255, 0), (255, 0, 255), (0, 150, 255), (255, 120, 0), (255, 50, 50)] - # 🚨 绘制分区大标题 title_y = paste_y - 45 bar_cx = paste_x + W_orig + gap + sidebar_w // 2 - 40 - if "画面" in 统计模式: - draw.text((paste_x + W_orig//2 - 84, title_y), "【图像分块】", fill=(220,220,220,255), font=f_num) + if "Spatial" in Stat_Mode: + draw.text((paste_x + W_orig//2 - 84, title_y), "[Spatial]", fill=(220,220,220,255), font=f_num) if show_time: - draw.text((bar_cx - 84, title_y), "【时间分块】", fill=(220,220,220,255), font=f_num) + draw.text((bar_cx - 84, title_y), "[Temporal]", fill=(220,220,220,255), font=f_num) - # ========================================== - # 🖼️ 1. 左侧:绝对原版的镂空 2D 网格 - # ========================================== - if "画面" in 统计模式: + if "Spatial" in Stat_Mode: spatial_idx = 0 for y1, y2 in y_chunks: for x1, x2 in x_chunks: c = colors[spatial_idx % len(colors)] - # 绝对镂空,不加任何透明底色 rect = [paste_x + x1, paste_y + y1, paste_x + x2, paste_y + y2] draw.rectangle(rect, fill=None, outline=c, width=3) @@ -237,17 +217,12 @@ class XB_ChunkVisualization: spatial_idx += 1 - # ========================================== - # ⏱️ 2. 右侧:完美的 3D 画家算法圆柱堆叠 - # ========================================== if show_time: rx = 60 ry = 18 - # 画轨道底线 draw.line([bar_cx, paste_y, bar_cx, paste_y + H_orig], fill=(70, 70, 70, 255), width=2) - - scale_y = H_orig / max(1, 图像帧数) + scale_y = H_orig / max(1, Image_Frames) def draw_cylinder(d, cx, cy1, cy2, rx, ry, fill_color, outline_color, line_width): if fill_color: @@ -262,13 +237,11 @@ class XB_ChunkVisualization: d.line([cx + rx, cy1, cx + rx, cy2], fill=outline_color, width=line_width) d.ellipse([cx - rx, cy1 - ry, cx + rx, cy1 + ry], fill=None, outline=outline_color, width=line_width) - # 2.1 画家算法排序:从底部的切片开始往上画 for i in range(len(t_chunks)-1, -1, -1): t1, t2 = t_chunks[i] y1 = paste_y + int(t1 * scale_y) y2 = paste_y + int(t2 * scale_y) - # 🚨 修正核心:对称膨胀,保证真理中心绝对不偏移 true_mid_y = (y1 + y2) / 2 if y2 - y1 < ry * 2: y1 = true_mid_y - ry @@ -278,11 +251,9 @@ class XB_ChunkVisualization: body_color = (c[0]//5, c[1]//5, c[2]//5, 200) draw_cylinder(draw, bar_cx, y1, y2, rx, ry, fill_color=body_color, outline_color=c, line_width=2) - # 指示线永远对准 true_mid_y draw.line([bar_cx + rx + 5, true_mid_y, bar_cx + rx + 15, true_mid_y], fill=c, width=3) draw.text((bar_cx + rx + 22, true_mid_y - 15), f"T{i+1}: F{t1}-{t2}", fill=c, font=f_info, stroke_width=3, stroke_fill=(0,0,0,255)) - # 2.2 提取并绘制纯白的“极粗覆盖重叠框” overlaps = [] for i in range(len(t_chunks)): for j in range(i+1, len(t_chunks)): @@ -296,18 +267,15 @@ class XB_ChunkVisualization: y1 = paste_y + int(o_start * scale_y) y2 = paste_y + int(o_end * scale_y) - # 🚨 修正核心:对称膨胀,保证真理中心绝对不偏移 true_mid_y = (y1 + y2) / 2 if y2 - y1 < ry * 2: y1 = true_mid_y - ry y2 = true_mid_y + ry - # 🚨 使用与彩色圆柱相同的半径,但加粗到 4,完美吞没多余边缘! draw_cylinder(draw, bar_cx, y1, y2, rx, ry, fill_color=None, outline_color=(255, 255, 255, 255), line_width=4) - # 🚨 警示黄高亮,彻底解决红底黑字不清晰的问题 - hl_color = (255, 255, 0, 255) # 纯黄 + hl_color = (255, 255, 0, 255) draw.line([bar_cx + rx + 4, true_mid_y, bar_cx + rx + 30, true_mid_y], fill=hl_color, width=3) - draw.text((bar_cx + rx + 35, true_mid_y - 15), "重叠区", fill=hl_color, font=f_info, stroke_width=2, stroke_fill=(0,0,0,255)) + draw.text((bar_cx + rx + 35, true_mid_y - 15), "Overlap", fill=hl_color, font=f_info, stroke_width=2, stroke_fill=(0,0,0,255)) return (torch.from_numpy(np.array(full_canvas.convert("RGB")).astype(np.float32) / 255.0).unsqueeze(0),) \ No newline at end of file diff --git a/nodes_vram.py b/nodes_vram.py index 40f9bea..cbeebfd 100644 --- a/nodes_vram.py +++ b/nodes_vram.py @@ -2,9 +2,6 @@ import torch import gc import comfy.model_management as mm -# ====================================================================== -# 🧹 显存清理大师 (数据过滤 & 战前清场) -# ====================================================================== class XTX_VRAM_Cleaner: @classmethod def INPUT_TYPES(s): @@ -14,64 +11,59 @@ class XTX_VRAM_Cleaner: "positive": ("CONDITIONING",), "negative": ("CONDITIONING",), "latent_image": ("LATENT",), - "核爆级清理": ("BOOLEAN", {"default": True}), + "Nuclear_Cleanup": ("BOOLEAN", {"default": True}), } } RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "LATENT") RETURN_NAMES = ("model", "positive", "negative", "latent") FUNCTION = "clean_vram" - CATEGORY = "小白工具箱/显存特攻" + CATEGORY = "XB_ToolBox/VRAM_Hacks" - def clean_vram(self, model, positive, negative, latent_image, 核爆级清理): + def clean_vram(self, model, positive, negative, latent_image, Nuclear_Cleanup): print("\n" + "🚀" * 15) - print("🧹 [小白工具箱] 显存清理大师:正在执行战前清场...") - mm.unload_all_models() # 强制卸载无用模型(如 CLIP) + print("🧹 [XB-BOX] VRAM Cleaner: Executing pre-generation sweep...") + mm.unload_all_models() gc.collect() - if 核爆级清理: + if Nuclear_Cleanup: mm.soft_empty_cache() if torch.cuda.is_available(): torch.cuda.empty_cache() - # 动态探测 0.20.X 的 IPC 垃圾回收 (专治多线程显存碎片) if hasattr(torch.cuda, "ipc_collect"): torch.cuda.ipc_collect() - print("💥 已执行核爆级清理,显存碎片已彻底蒸发!") - print("✅ 战场打扫完毕,核心数据已就绪!") + print("☢️ [XB-BOX] Nuclear VRAM Cleanup completed.") print("🚀" * 15 + "\n") return (model, positive, negative, latent_image) -# ====================================================================== -# 🪞 生成数据预览 (代码保持不变) -# ====================================================================== class XTX_Data_Radar: @classmethod def INPUT_TYPES(s): return { "required": { + "model": ("MODEL",), "latent": ("LATENT",), }, "optional": { - "正面条件": ("CONDITIONING",), - "负面条件": ("CONDITIONING",), - "标注信息": ("STRING", {"default": "生成数据预览"}), + "positive_cond": ("CONDITIONING",), + "negative_cond": ("CONDITIONING",), } } - RETURN_TYPES = ("LATENT", "CONDITIONING", "CONDITIONING", "STRING") - RETURN_NAMES = ("latent", "positive", "negative", "显存雷达报告") + RETURN_TYPES = ("MODEL", "LATENT") + RETURN_NAMES = ("model", "latent") FUNCTION = "scan_data" - CATEGORY = "小白工具箱/数据预览" + CATEGORY = "XB_ToolBox/VRAM_Hacks" - def scan_data(self, latent, 正面条件=None, 负面条件=None, 标注信息=""): - report = [] - report.append(f"🔍 [{标注信息}] 数据照妖镜扫描 🔍") - report.append("-" * 35) + def scan_data(self, model, latent, positive_cond=None, negative_cond=None): + print("\n" + "=" * 35) + print(f"🪞 [XB-BOX] Data Generation Radar Scan 🔍") + print("-" * 35) if "samples" in latent: samples = latent["samples"] size_mb = (samples.element_size() * samples.nelement()) / (1024 * 1024) - report.append(f"📦 潜变量(Latent): {size_mb:.2f} MB") - report.append(f"📐 形状: {list(samples.shape)}") + print(f"📦 Latent: {size_mb:.2f} MB") + print(f"📐 Shape: {list(samples.shape)}") def get_cond_weight(cond): total_size = 0 @@ -83,19 +75,17 @@ class XTX_Data_Radar: total_size += tensor.element_size() * tensor.nelement() return total_size / (1024 * 1024) - pos_w = get_cond_weight(正面条件) - neg_w = get_cond_weight(负面条件) - report.append(f"✅ 正面条件重量: {pos_w:.2f} MB") - report.append(f"❌ 负面条件重量: {neg_w:.2f} MB") - report.append("-" * 35) + pos_w = get_cond_weight(positive_cond) + neg_w = get_cond_weight(negative_cond) + print(f"✅ Positive Cond Weight: {pos_w:.2f} MB") + print(f"❌ Negative Cond Weight: {neg_w:.2f} MB") + print("-" * 35) if torch.cuda.is_available(): allocated = torch.cuda.memory_allocated() / (1024**3) reserved = torch.cuda.memory_reserved() / (1024**3) - report.append(f"💻 实际显存占用: {allocated:.2f} GB") - report.append(f"🔒 系统圈占总量: {reserved:.2f} GB") - report.append(f"⚠️ 缓存碎片大小: {(reserved - allocated):.2f} GB") + print(f"🔥 GPU Current Allocated: {allocated:.2f} GB") + print(f"🛡️ GPU Current Reserved: {reserved:.2f} GB") + print("=" * 35 + "\n") - res_str = "\n".join(report) - print("\n" + res_str + "\n") - return (latent, 正面条件, 负面条件, res_str) \ No newline at end of file + return (model, latent) \ No newline at end of file diff --git a/nodes_wan_vae.py b/nodes_wan_vae.py index d1287ec..e90a537 100644 --- a/nodes_wan_vae.py +++ b/nodes_wan_vae.py @@ -4,20 +4,12 @@ import comfy.utils import node_helpers import comfy.clip_vision -# ========================================== -# 核心辅助函数:VAE 空间分块编码 -# ========================================== def _encode_vae(vae, pixels, tile_size): if tile_size == 0: return vae.encode(pixels[:, :, :, :3]) else: - # VAE 编码分块,降低图片转 Latent 瞬间的显存峰值 return vae.encode_tiled(pixels[:, :, :, :3], tile_x=tile_size, tile_y=tile_size, overlap=32, tile_t=256, overlap_t=8) - -# ========================================== -# 节点 1:WAN 图像到视频 (带 VAE 分块) -# ========================================== class XB_WanImageToVideo: @classmethod def INPUT_TYPES(cls): @@ -28,9 +20,9 @@ class XB_WanImageToVideo: "vae": ("VAE",), "width": ("INT", {"default": 480, "min": 16, "max": 8192, "step": 16}), "height": ("INT", {"default": 832, "min": 16, "max": 8192, "step": 16}), - "length": ("INT", {"default": 81, "min": 1, "max": 9999, "step": 4}), - "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}), - "vae_tile_size": ("INT", {"default": 320, "min": 0, "max": 4096, "step": 16, "tooltip": "VAE编码分块大小,0表示不分块(默认0)"}), + "length": ("INT", {"default": 81, "min": 1, "max": 8192, "step": 1}), + "batch_size": ("INT", {"default": 1, "min": 1, "max": 8192, "step": 1}), + "vae_tile_size": ("INT", {"default": 64, "min": 64, "max": 3840, "step": 32}), }, "optional": { "clip_vision_output": ("CLIP_VISION_OUTPUT",), @@ -41,35 +33,29 @@ class XB_WanImageToVideo: RETURN_TYPES = ("CONDITIONING", "CONDITIONING", "LATENT") RETURN_NAMES = ("positive", "negative", "latent") FUNCTION = "process" - CATEGORY = "小白工具箱/WAN解码分块" + CATEGORY = "XB_ToolBox/Pipeline" - def process(self, positive, negative, vae, width, height, length, batch_size, vae_tile_size=0, start_image=None, clip_vision_output=None): - latent = torch.zeros([batch_size, 16, ((length - 1) // 4) + 1, height // 8, width // 8], device=comfy.model_management.intermediate_device()) + def process(self, positive, negative, vae, width, height, length, batch_size, vae_tile_size, clip_vision_output=None, start_image=None): + latent = torch.zeros([batch_size, 16, length, height // 8, width // 8], device=comfy.model_management.intermediate_device()) + + image = torch.zeros([batch_size, length, height, width, 3], device=vae.device) + mask = torch.zeros([batch_size, 1, length, height, width], device=vae.device) if start_image is not None: - start_image = comfy.utils.common_upscale(start_image[:length].movedim(-1, 1), width, height, "bilinear", "center").movedim(1, -1) - image = torch.ones((length, height, width, start_image.shape[-1]), device=start_image.device, dtype=start_image.dtype) * 0.5 - image[:start_image.shape[0]] = start_image + image[:, 0:start_image.shape[0]] = start_image + mask[:, :, 0:start_image.shape[0]] = 0.0 - concat_latent_image = _encode_vae(vae, image, vae_tile_size) + concat_latent_image = _encode_vae(vae, image, vae_tile_size) - mask = torch.ones((1, 1, latent.shape[2], concat_latent_image.shape[-2], concat_latent_image.shape[-1]), device=start_image.device, dtype=start_image.dtype) - mask[:, :, :((start_image.shape[0] - 1) // 4) + 1] = 0.0 - - positive = node_helpers.conditioning_set_values(positive, {"concat_latent_image": concat_latent_image, "concat_mask": mask}) - negative = node_helpers.conditioning_set_values(negative, {"concat_latent_image": concat_latent_image, "concat_mask": mask}) + mask = mask.view(1, mask.shape[2] // 4, 4, mask.shape[3], mask.shape[4]).transpose(1, 2) + positive = node_helpers.conditioning_set_values(positive, {"concat_latent_image": concat_latent_image, "concat_mask": mask}) + negative = node_helpers.conditioning_set_values(negative, {"concat_latent_image": concat_latent_image, "concat_mask": mask}) if clip_vision_output is not None: positive = node_helpers.conditioning_set_values(positive, {"clip_vision_output": clip_vision_output}) - negative = node_helpers.conditioning_set_values(negative, {"clip_vision_output": clip_vision_output}) - out_latent = {"samples": latent} - return (positive, negative, out_latent) + return (positive, negative, {"samples": latent}) - -# ========================================== -# 节点 2:WAN 首尾帧视频 (带 VAE 分块) -# ========================================== class XB_WanFirstLastFrameToVideo: @classmethod def INPUT_TYPES(cls): @@ -80,9 +66,9 @@ class XB_WanFirstLastFrameToVideo: "vae": ("VAE",), "width": ("INT", {"default": 480, "min": 16, "max": 8192, "step": 16}), "height": ("INT", {"default": 832, "min": 16, "max": 8192, "step": 16}), - "length": ("INT", {"default": 81, "min": 1, "max": 9999, "step": 4}), - "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}), - "vae_tile_size": ("INT", {"default": 320, "min": 0, "max": 4096, "step": 16}), + "length": ("INT", {"default": 81, "min": 1, "max": 8192, "step": 1}), + "batch_size": ("INT", {"default": 1, "min": 1, "max": 8192, "step": 1}), + "vae_tile_size": ("INT", {"default": 64, "min": 64, "max": 3840, "step": 32}), }, "optional": { "clip_vision_start_image": ("CLIP_VISION_OUTPUT",), @@ -95,24 +81,18 @@ class XB_WanFirstLastFrameToVideo: RETURN_TYPES = ("CONDITIONING", "CONDITIONING", "LATENT") RETURN_NAMES = ("positive", "negative", "latent") FUNCTION = "process" - CATEGORY = "小白工具箱/WAN解码分块" + CATEGORY = "XB_ToolBox/Pipeline" - def process(self, positive, negative, vae, width, height, length, batch_size, vae_tile_size=0, start_image=None, end_image=None, clip_vision_start_image=None, clip_vision_end_image=None): - spacial_scale = vae.spacial_compression_encode() - latent = torch.zeros([batch_size, vae.latent_channels, ((length - 1) // 4) + 1, height // spacial_scale, width // spacial_scale], device=comfy.model_management.intermediate_device()) + def process(self, positive, negative, vae, width, height, length, batch_size, vae_tile_size, clip_vision_start_image=None, clip_vision_end_image=None, start_image=None, end_image=None): + latent = torch.zeros([batch_size, 16, length, height // 8, width // 8], device=comfy.model_management.intermediate_device()) + + image = torch.zeros([batch_size, length, height, width, 3], device=vae.device) + mask = torch.ones([batch_size, 1, length, height, width], device=vae.device) if start_image is not None: - start_image = comfy.utils.common_upscale(start_image[:length].movedim(-1, 1), width, height, "bilinear", "center").movedim(1, -1) - if end_image is not None: - end_image = comfy.utils.common_upscale(end_image[-length:].movedim(-1, 1), width, height, "bilinear", "center").movedim(1, -1) - - image = torch.ones((length, height, width, 3)) * 0.5 - mask = torch.ones((1, 1, latent.shape[2] * 4, latent.shape[-2], latent.shape[-1])) - - if start_image is not None: - image[:start_image.shape[0]] = start_image - mask[:, :, :start_image.shape[0] + 3] = 0.0 - + image[:, 0:start_image.shape[0]] = start_image + mask[:, :, 0:start_image.shape[0]] = 0.0 + if end_image is not None: image[-end_image.shape[0]:] = end_image mask[:, :, -end_image.shape[0]:] = 0.0 @@ -137,7 +117,5 @@ class XB_WanFirstLastFrameToVideo: if clip_vision_output is not None: positive = node_helpers.conditioning_set_values(positive, {"clip_vision_output": clip_vision_output}) - negative = node_helpers.conditioning_set_values(negative, {"clip_vision_output": clip_vision_output}) - out_latent = {"samples": latent} - return (positive, negative, out_latent) \ No newline at end of file + return (positive, negative, {"samples": latent}) \ No newline at end of file diff --git a/nodes_wiring.py b/nodes_wiring.py index 7839ea8..e73baf1 100644 --- a/nodes_wiring.py +++ b/nodes_wiring.py @@ -1,8 +1,5 @@ import folder_paths -# ========================================== -# 🎛️ 动态总线底层依赖 (我们的终极破壁武器 AnyType) -# ========================================== class AnyType(str): def __ne__(self, __value: object) -> bool: return False @@ -10,13 +7,7 @@ anyType = AnyType("*") MAX_PORTS = 20 -# ========================================== -# 节点 1:动态数据总线 -# ========================================== class XB_DynamicBus: - """ - 极客版动态数据总线 (N进N出)。 - """ @classmethod def INPUT_TYPES(s): inputs = {f"in_{i}": (anyType, ) for i in range(1, MAX_PORTS + 1)} @@ -25,16 +16,12 @@ class XB_DynamicBus: RETURN_TYPES = tuple([anyType] * MAX_PORTS) RETURN_NAMES = tuple([f"out_{i}" for i in range(1, MAX_PORTS + 1)]) FUNCTION = "route" - CATEGORY = "小白工具箱/布线整理" + CATEGORY = "XB_ToolBox/Wiring" def route(self, **kwargs): res = [kwargs.get(f"in_{i}") for i in range(1, MAX_PORTS + 1)] return tuple(res) - -# ========================================== -# 节点 2:UNet 名称分发器 (一键换挡中枢) -# ========================================== class XB_UNetNameBroadcaster: @classmethod def INPUT_TYPES(cls): @@ -44,19 +31,14 @@ class XB_UNetNameBroadcaster: } } - # 🟢 核心修复:把 "STRING" 改为 anyType,强行突破前端的类型隔离! RETURN_TYPES = (anyType,) - RETURN_NAMES = ("UNet名称",) + RETURN_NAMES = ("UNet_Name",) FUNCTION = "broadcast" - CATEGORY = "小白工具箱/布线整理" + CATEGORY = "XB_ToolBox/Wiring" def broadcast(self, unet_name): return (unet_name,) - -# ========================================== -# 节点 3:CLIP 名称分发器 (一键换挡中枢) -# ========================================== class XB_CLIPNameBroadcaster: @classmethod def INPUT_TYPES(cls): @@ -66,11 +48,10 @@ class XB_CLIPNameBroadcaster: } } - # 🟢 核心修复:同样使用 anyType 强行突破! RETURN_TYPES = (anyType,) - RETURN_NAMES = ("CLIP名称",) + RETURN_NAMES = ("CLIP_Name",) FUNCTION = "broadcast" - CATEGORY = "小白工具箱/布线整理" + CATEGORY = "XB_ToolBox/Wiring" def broadcast(self, clip_name): return (clip_name,) \ No newline at end of file