Add files via upload

This commit is contained in:
wjluoxiao
2026-05-15 21:05:45 +08:00
committed by GitHub
parent 7a74ade686
commit 62f1c8dac4
21 changed files with 806 additions and 571 deletions
+73 -73
View File
@@ -1,74 +1,74 @@
<div align="center">
🌐 <a href="README.md">English</a> | <a href="README_zh.md">简体中文</a>
</div>
<br>
# 🧰 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.
<div align="center">
🌐 <a href="README.md">English</a> | <a href="README_zh.md">简体中文</a>
</div>
<br>
# 🧰 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.
---
+28 -30
View File
@@ -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")
+5 -11
View File
@@ -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;
};
}
+17 -45
View File
@@ -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 = `<span style="color: #fff; font-weight: bold; font-size: 16px;">📦 批量选择要克隆的组件</span>`;
header.innerHTML = `<span style="color: #fff; font-weight: bold; font-size: 16px;">📦 Batch Select Components to Clone</span>`;
const closeBtn = document.createElement("button");
closeBtn.innerText = "✖";
@@ -222,7 +204,7 @@ function showMultiPickerModal(validNodes, onConfirmCallback) {
const checkboxes = [];
if (validNodes.length === 0) {
listContainer.innerHTML = `<div style="padding: 20px; text-align: center; color: #666;">当前工作流无可用节点。</div>`;
listContainer.innerHTML = `<div style="padding: 20px; text-align: center; color: #666;">No available nodes in current workflow.</div>`;
} 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 = `
<span style="font-weight: bold; font-size: 14px; color: #fff;">${node.title || node.type}</span>
<span style="font-size: 11px; color: #667; margin-top: 4px;">ID: ${node.id} | 类型: ${node.type}</span>
<span style="font-size: 11px; color: #667; margin-top: 4px;">ID: ${node.id} | Type: ${node.type}</span>
`;
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();
+7 -21
View File
@@ -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");
+6 -8
View File
@@ -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);
+4 -17
View File
@@ -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;
+9 -18
View File
@@ -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();
+49
View File
@@ -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": "确定"
}
+321
View File
@@ -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": "模型" }
}
}
}
+6 -17
View File
@@ -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)
+12 -26
View File
@@ -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:
+1 -2
View File
@@ -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 ()
+43 -65
View File
@@ -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)
+81
View File
@@ -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,)
+4 -9
View File
@@ -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,
+17 -23
View File
@@ -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
+61 -93
View File
@@ -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),)
+28 -38
View File
@@ -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)
return (model, latent)
+29 -51
View File
@@ -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)
return (positive, negative, {"samples": latent})
+5 -24
View File
@@ -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,)