Add files via upload
This commit is contained in:
@@ -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).
|
||||
|
||||
---
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
### ✨ 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).
|
||||
|
||||
---
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
### ✨ 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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": "确定"
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,)
|
||||
Reference in New Issue
Block a user