Merge pull request #14 from ModelTC/gp/update-lightx2v

Gp/update lightx2v
This commit is contained in:
PengGao
2026-10-23 15:50:27 +08:00
committed by GitHub
27 changed files with 1069 additions and 1275 deletions
+1
View File
@@ -6,6 +6,7 @@ on:
- "v*.*.*"
permissions:
contents: read
issues: write
jobs:
+27
View File
@@ -0,0 +1,27 @@
name: Quality checks
on:
push:
pull_request:
permissions:
contents: read
jobs:
pre-commit:
name: Ruff and repository checks
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install pre-commit
run: python -m pip install pre-commit
- name: Run checks
run: pre-commit run --all-files
+1 -1
View File
@@ -346,4 +346,4 @@ Temporary Items
CLAUDE.md
AGENTS.md
.gitnexus/
.claude/
.claude/
+32
View File
@@ -0,0 +1,32 @@
# Contributing
## Development setup
The wrapper follows the Python version supported by the bundled LightX2V
submodule (Python 3.10 or newer).
```bash
git clone --recursive https://github.com/ModelTC/ComfyUI-Lightx2vWrapper.git
cd ComfyUI-Lightx2vWrapper
python -m pip install -r lightx2v/requirements.txt
python -m pip install -r requirements.txt pre-commit
pre-commit install
```
## Before opening a pull request
Run the repository checks from the project root:
```bash
pre-commit run --all-files
```
For workflow or runner changes, validate the affected ComfyUI workflow with a
local LightX2V installation and include the workflow or reproduction details
in the pull request description. Keep changes to the `lightx2v` submodule in a
separate upstream contribution unless the wrapper specifically requires a
submodule revision update.
Pull requests should explain the user-visible behavior, list validation
performed, and avoid committing model weights, generated media, credentials,
or local debugging artifacts.
+62 -167
View File
@@ -1,202 +1,97 @@
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Python](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org/downloads/) [![ComfyUI](https://img.shields.io/badge/ComfyUI-Custom%20Node-brightgreen)](https://github.com/comfyanonymous/ComfyUI) [![Hugging Face](https://img.shields.io/badge/%F0%9F%A4%97%20Models-lightx2v-yellow)](https://huggingface.co/lightx2v) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/GacLove/ComfyUI-Lightx2vWrapper)
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/)
[![ComfyUI](https://img.shields.io/badge/ComfyUI-Custom%20Node-brightgreen)](https://github.com/comfyanonymous/ComfyUI)
[![Models](https://img.shields.io/badge/models-LightX2V-yellow)](https://huggingface.co/lightx2v)
# ComfyUI-Lightx2vWrapper
[中文版](./README_CN.md) | English
ComfyUI custom nodes for [LightX2V](https://github.com/ModelTC/LightX2V). The
wrapper exposes LightX2V's modular configuration and runner APIs, together
with ComfyUI-friendly file inputs and output previews.
A ComfyUI custom node wrapper for LightX2V, enabling modular video generation with advanced optimization features.
[中文文档](README_CN.md)
## Features
## Supported workflows
- **Modular Configuration System**: Separate nodes for each aspect of video generation
- **Text-to-Video (T2V) and Image-to-Video (I2V)**: Support for both generation modes
- **Advanced Optimizations**:
- TeaCache acceleration (up to 3x speedup)
- Quantization support (int8, fp8)
- Memory optimization with CPU offloading
- Lightweight VAE options
- **LoRA Support**: Chain multiple LoRA models for customization
- **Multiple Model Support**: wan2.1, hunyuan architectures
The repository includes runnable workflow examples under
[`example_workflows/`](example_workflows/):
- [Wan 2.2](example_workflows/lighx2v_wan2.2.json) text/image-to-video
- [SekoTalk](example_workflows/lightx2v_seko_talk.json) and [static
SekoTalk](example_workflows/lightx2v_seko_talk_static.json)
- [Multi-person SekoTalk](example_workflows/lightx2v_seko_talk_multi_talk.json)
- [SeedVR2](example_workflows/lightx2v_seedvr.json) video restoration
- [SwiftVR image](example_workflows/lightx2v_swiftvr_image.json) and [SwiftVR
video](example_workflows/lightx2v_swiftvr_video.json) restoration
The examples are versioned with the wrapper and are the best reference for
current node names and connections.
## Installation
1. Clone this repository with submodules into your ComfyUI's `custom_nodes` directory:
Clone the repository with its LightX2V submodule into ComfyUI's
`custom_nodes` directory:
```bash
cd ComfyUI/custom_nodes
git clone --recursive https://github.com/gaclove/ComfyUI-Lightx2vWrapper.git
git clone --recursive https://github.com/ModelTC/ComfyUI-Lightx2vWrapper.git
cd ComfyUI-Lightx2vWrapper
python -m pip install -r lightx2v/requirements.txt
python -m pip install -r requirements.txt
```
If you already cloned without submodules, initialize them:
For an existing checkout, update the submodule with:
```bash
cd ComfyUI-Lightx2vWrapper
git submodule update --init --recursive
```
2. Install dependencies:
Download the model files from [LightX2V on Hugging Face](https://huggingface.co/lightx2v)
and place them under `ComfyUI/models/lightx2v/`.
```bash
cd ComfyUI-Lightx2vWrapper
# Install lightx2v submodule dependencies
pip install -r lightx2v/requirements.txt
# Install ComfyUI wrapper dependencies
pip install -r requirements.txt
```
## Node groups
3. Download models and place them in `ComfyUI/models/lightx2v/` directory
- **Configuration:** `LightX2VInferenceConfig`, `LightX2VTeaCache`,
`LightX2VQuantization`, and `LightX2VMemoryOptimization`.
- **Composition:** `LightX2VConfigCombinerV2` and `LightX2VConfigCombinerV3`.
- **Inference:** `LightX2VModularInferenceV2`.
- **Talk inputs:** single-object, file-list, JSON, and multi-object talk
combiners.
- **Video inputs and restoration:** `LightX2VInputVideoPath`, SeedVR2 and
SwiftVR loaders/samplers, plus `LightX2VOutputVideoPreview`.
- **LoRA:** `LightX2VLoRALoader`.
## Node Overview
The inference node currently accepts the model classes exposed by
`model_utils.support_model_cls_list`: Wan 2.1/2.2 variants, SekoTalk,
Qwen Image, and SeedVR2. The retired `*_distill` values remain selectable as
compatibility aliases for saved ComfyUI workflows and are normalized before
LightX2V's runner is built.
### Configuration Nodes
## Model files
#### 1. LightX2V Inference Config
Place each model in its own directory under `ComfyUI/models/lightx2v/`. LoRA
files can be placed in either `ComfyUI/models/lightx2v/loras/` or
`ComfyUI/models/loras/`.
Basic inference configuration for video generation.
- **Inputs**: model, task_type, inference_steps, seed, cfg_scale, width, height, video_length, fps
- **Output**: Base configuration object
#### 2. LightX2V TeaCache
Feature caching acceleration configuration.
- **Inputs**: enable, threshold (0.0-1.0), use_ret_steps
- **Output**: TeaCache configuration
- **Note**: Lower threshold = more speedup (0.1 ~2x, 0.2 ~3x)
#### 3. LightX2V Quantization
Model quantization settings for memory efficiency.
- **Inputs**: dit_precision, t5_precision, clip_precision, backend, sensitive_layers_precision
- **Output**: Quantization configuration
- **Backends**: Auto-detected (vllm, sgl, q8f)
#### 4. LightX2V Memory Optimization
Memory management strategies.
- **Inputs**: optimization_level, attention_type, enable_rotary_chunking, cpu_offload, unload_after_generate
- **Output**: Memory optimization configuration
#### 5. LightX2V Lightweight VAE
VAE optimization options.
- **Inputs**: use_tiny_vae, use_tiling_vae
- **Output**: VAE configuration
#### 6. LightX2V LoRA Loader
Load and chain LoRA models.
- **Inputs**: lora_name, strength (0.0-2.0), lora_chain (optional)
- **Output**: LoRA chain configuration
### Combination Node
#### 7. LightX2V Config Combiner
Combines all configuration modules into a single configuration.
- **Inputs**: All configuration types (optional)
- **Output**: Combined configuration object
### Inference Node
#### 8. LightX2V Modular Inference
Main inference node for video generation.
- **Inputs**: combined_config, prompt, negative_prompt, image (optional), audio (optional)
- **Outputs**: Generated video frames
## Usage Examples
### Basic T2V Workflow
1. Create LightX2V Inference Config (task_type: "t2v")
2. Use LightX2V Config Combiner
3. Connect to LightX2V Modular Inference with text prompt
4. Save video output
### I2V with Optimizations
1. Load input image
2. Create LightX2V Inference Config (task_type: "i2v")
3. Add LightX2V TeaCache (threshold: 0.26)
4. Add LightX2V Memory Optimization
5. Combine configs with LightX2V Config Combiner
6. Run LightX2V Modular Inference
### With LoRA
1. Create base configuration
2. Load LoRA with LightX2V LoRA Loader
3. Chain multiple LoRAs if needed
4. Combine all configs
5. Run inference
## Model Directory Structure
Download models from: <https://huggingface.co/lightx2v>
Models should be placed in:
```txt
```text
ComfyUI/models/lightx2v/
├── Wan2.1-I2V-14B-720P-xxx/ # Main model checkpoints
├── Wan2.1-I2V-14B-480P-xxx/ # Main model checkpoints
├── loras/ # LoRA models
├── Wan2.2-I2V-A14B-distill-fp8/
├── seedvr2_ema_3b_fp8/
└── loras/
```
## Tips
The exact directory names depend on the downloaded checkpoint. The model
picker only lists directories that exist on the local ComfyUI installation.
- Start with default settings and adjust based on your hardware
- Use TeaCache with threshold 0.1-0.2 for significant speedup
- Enable memory optimization if running on limited VRAM
- Quantization can reduce memory usage but may affect quality
- Chain multiple LoRAs for complex style combinations
## Development
## Troubleshooting
- **Out of Memory**: Enable memory optimization or use quantization
- **Slow Generation**: Enable TeaCache or reduce inference steps
- **Model Not Found**: Check model paths in `ComfyUI/models/lightx2v/`
## Example Workflows
Example workflow JSON files are provided in the `examples/` directory:
- `wan_i2v.json` - Basic image-to-video
- `wan_i2v_with_distill_lora.json` - I2V with distillation LoRA
- `wan_t2v_with_distill_lora.json` - T2V with distillation LoRA
## Contributing Guidelines
We welcome community contributions! Before submitting code, please ensure you follow these steps:
### Install Development Dependencies
```bash
pip install ruff pre-commit
```
### Code Quality Check
Before committing code, run the following command:
Install the development hook and run the same checks used by CI:
```bash
python -m pip install pre-commit
pre-commit install
pre-commit run --all-files
```
This will automatically check code formatting, syntax errors, and other code quality issues.
### Contribution Process
1. Fork this repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Create a Pull Request
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for pull request and workflow
validation guidelines.
+55 -168
View File
@@ -1,202 +1,89 @@
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Python](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org/downloads/) [![ComfyUI](https://img.shields.io/badge/ComfyUI-自定义节点-brightgreen)](https://github.com/comfyanonymous/ComfyUI) [![Hugging Face](https://img.shields.io/badge/%F0%9F%A4%97%20模型-lightx2v-yellow)](https://huggingface.co/lightx2v) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/GacLove/ComfyUI-Lightx2vWrapper)
[![许可证](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/)
[![ComfyUI](https://img.shields.io/badge/ComfyUI-自定义节点-brightgreen)](https://github.com/comfyanonymous/ComfyUI)
[![模型](https://img.shields.io/badge/models-LightX2V-yellow)](https://huggingface.co/lightx2v)
# ComfyUI-Lightx2vWrapper
[English](./README.md) | 中文版
[English](README.md) | 中文
LightX2V 的 ComfyUI 自定义节点封装,通过模块化配置实现视频生成和高级优化功能。
[LightX2V](https://github.com/ModelTC/LightX2V) 的 ComfyUI 自定义节点封装,
提供模块化配置、ComfyUI 文件输入以及视频输出预览。
## 功能特性
## 已提供的工作流
- **模块化配置系统**:为视频生成的各个方面提供独立的配置节点
- **文本生成视频(T2V)和图像生成视频(I2V)**:支持两种生成模式
- **高级优化功能**:
- TeaCache 加速(最高可达 3 倍速度提升)
- 量化支持(int8、fp8)
- 内存优化与 CPU 卸载
- 轻量级 VAE 选项
- **LoRA 支持**:可串联多个 LoRA 模型进行自定义
- **多模型支持**:wan2.1、hunyuan 架构
仓库在 [`example_workflows/`](example_workflows/) 中提供了与当前节点注册表
同步的示例:
## 安装说明
- [Wan 2.2](example_workflows/lighx2v_wan2.2.json) 文生视频/图生视频
- [SekoTalk](example_workflows/lightx2v_seko_talk.json) 和[静态
SekoTalk](example_workflows/lightx2v_seko_talk_static.json)
- [多人 SekoTalk](example_workflows/lightx2v_seko_talk_multi_talk.json)
- [SeedVR2](example_workflows/lightx2v_seedvr.json) 视频修复
- [SwiftVR 图像](example_workflows/lightx2v_swiftvr_image.json) 和 [SwiftVR
视频](example_workflows/lightx2v_swiftvr_video.json) 修复
1. 将此仓库克隆到 ComfyUI 的 `custom_nodes` 目录,包含子模块:
示例工作流会随封装器一起更新,可作为当前节点名称和连线方式的参考。
## 安装
将仓库和 LightX2V 子模块一起克隆到 ComfyUI 的 `custom_nodes` 目录:
```bash
cd ComfyUI/custom_nodes
git clone --recursive https://github.com/gaclove/ComfyUI-Lightx2vWrapper.git
git clone --recursive https://github.com/ModelTC/ComfyUI-Lightx2vWrapper.git
cd ComfyUI-Lightx2vWrapper
python -m pip install -r lightx2v/requirements.txt
python -m pip install -r requirements.txt
```
如果已经克隆但未包含子模块,请初始化子模块:
如果已有仓库但还没有子模块:
```bash
cd ComfyUI-Lightx2vWrapper
git submodule update --init --recursive
```
2. 安装依赖:
从 [Hugging Face 的 LightX2V 页面](https://huggingface.co/lightx2v) 下载模型,
放到 `ComfyUI/models/lightx2v/` 下。
```bash
cd ComfyUI-Lightx2vWrapper
# 安装 lightx2v 子模块依赖
pip install -r lightx2v/requirements.txt
# 安装 ComfyUI 封装器依赖
pip install -r requirements.txt
```
## 节点分组
3. 下载模型并放置在 `ComfyUI/models/lightx2v/` 目录
- **配置:** `LightX2VInferenceConfig`、`LightX2VTeaCache`、
`LightX2VQuantization`、`LightX2VMemoryOptimization`
- **组合:** `LightX2VConfigCombinerV2`、`LightX2VConfigCombinerV3`
- **推理:** `LightX2VModularInferenceV2`
- **Talk 输入:** 单对象、文件列表、JSON 和多人 Talk 组合节点
- **视频输入与修复:** `LightX2VInputVideoPath`、SeedVR2/SwiftVR 加载器与
采样器、`LightX2VOutputVideoPreview`
- **LoRA:** `LightX2VLoRALoader`
## 节点概览
推理节点当前支持 `model_utils.support_model_cls_list` 中的 Wan 2.1/2.2
变体、SekoTalk、Qwen Image 和 SeedVR2。旧 workflow 使用的 `*_distill`
值仍作为兼容别名保留,构建 LightX2V runner 前会自动归一化。
### 配置节点
## 模型目录
#### 1. LightX2V Inference Config(推理配置)
每个模型放在 `ComfyUI/models/lightx2v/` 下的独立目录中。LoRA 可以放在
`ComfyUI/models/lightx2v/loras/` 或 `ComfyUI/models/loras/`:
视频生成的基础推理配置。
- **输入**:model(模型)、task_type(任务类型)、inference_steps(推理步数)、seed(种子)、cfg_scale(CFG 缩放)、width(宽度)、height(高度)、video_length(视频长度)、fps(帧率)
- **输出**:基础配置对象
#### 2. LightX2V TeaCache(TeaCache 配置)
特征缓存加速配置。
- **输入**:enable(启用)、threshold(阈值 0.0-1.0)、use_ret_steps(使用返回步数)
- **输出**:TeaCache 配置
- **注意**:阈值越低 = 速度提升越大(0.1 约 2 倍,0.2 约 3 倍)
#### 3. LightX2V Quantization(量化配置)
模型量化设置,提升内存效率。
- **输入**:dit_precision(DIT 精度)、t5_precision(T5 精度)、clip_precision(CLIP 精度)、backend(后端)、sensitive_layers_precision(敏感层精度)
- **输出**:量化配置
- **后端**:自动检测(vllm、sgl、q8f)
#### 4. LightX2V Memory Optimization(内存优化)
内存管理策略。
- **输入**:optimization_level(优化级别)、attention_type(注意力类型)、enable_rotary_chunking(启用旋转分块)、cpu_offload(CPU 卸载)、unload_after_generate(生成后卸载)
- **输出**:内存优化配置
#### 5. LightX2V Lightweight VAE(轻量级 VAE)
VAE 优化选项。
- **输入**:use_tiny_vae(使用微型 VAE)、use_tiling_vae(使用平铺 VAE)
- **输出**:VAE 配置
#### 6. LightX2V LoRA Loader(LoRA 加载器)
加载并串联 LoRA 模型。
- **输入**:lora_name(LoRA 名称)、strength(强度 0.0-2.0)、lora_chain(LoRA 链,可选)
- **输出**:LoRA 链配置
### 组合节点
#### 7. LightX2V Config Combiner(配置组合器)
将所有配置模块组合成单一配置。
- **输入**:所有配置类型(可选)
- **输出**:组合后的配置对象
### 推理节点
#### 8. LightX2V Modular Inference(模块化推理)
视频生成的主推理节点。
- **输入**:combined_config(组合配置)、prompt(提示词)、negative_prompt(负面提示词)、image(图像,可选)、audio(音频,可选)
- **输出**:生成的视频帧
## 使用示例
### 基础 T2V 工作流
1. 创建 LightX2V Inference Config(task_type: "t2v")
2. 使用 LightX2V Config Combiner
3. 连接到 LightX2V Modular Inference,输入文本提示词
4. 保存视频输出
### 带优化的 I2V
1. 加载输入图像
2. 创建 LightX2V Inference Config(task_type: "i2v")
3. 添加 LightX2V TeaCache(threshold: 0.26)
4. 添加 LightX2V Memory Optimization
5. 使用 LightX2V Config Combiner 组合配置
6. 运行 LightX2V Modular Inference
### 使用 LoRA
1. 创建基础配置
2. 使用 LightX2V LoRA Loader 加载 LoRA
3. 如需要可串联多个 LoRA
4. 组合所有配置
5. 运行推理
## 模型目录结构
从以下地址下载模型:<https://huggingface.co/lightx2v>
模型应放置在:
```txt
```text
ComfyUI/models/lightx2v/
├── Wan2.1-I2V-14B-720P-xxx/ # 主模型检查点
├── Wan2.1-I2V-14B-480P-xxx/ # 主模型检查点
├── loras/ # LoRA 模型
├── Wan2.2-I2V-A14B-distill-fp8/
├── seedvr2_ema_3b_fp8/
└── loras/
```
## 使用技巧
具体目录名取决于下载的 checkpoint;模型选择器只会列出当前 ComfyUI 中存在
的目录。
- 从默认设置开始,根据硬件情况调整
- 使用 TeaCache,阈值设为 0.1-0.2 可显著提速
- 在显存有限时启用内存优化
- 量化可减少内存使用但可能影响质量
- 串联多个 LoRA 实现复杂风格组合
## 开发
## 故障排除
- **内存不足**:启用内存优化或使用量化
- **生成速度慢**:启用 TeaCache 或减少推理步数
- **找不到模型**:检查 `ComfyUI/models/lightx2v/` 中的模型路径
## 示例工作流
`examples/` 目录中提供了示例工作流 JSON 文件:
- `wan_i2v.json` - 基础图像生成视频
- `wan_i2v_with_distill_lora.json` - 带蒸馏 LoRA 的 I2V
- `wan_t2v_with_distill_lora.json` - 带蒸馏 LoRA 的 T2V
## 贡献指南
我们欢迎社区贡献!在提交代码之前,请确保遵循以下步骤:
### 安装开发依赖
```bash
pip install ruff pre-commit
```
### 代码质量检查
在提交代码之前,请运行以下命令:
安装 pre-commit 并运行 CI 使用的检查:
```bash
python -m pip install pre-commit
pre-commit install
pre-commit run --all-files
```
这将自动检查代码格式、语法错误和其他代码质量问题。
### 贡献流程
1. Fork 本仓库
2. 创建功能分支 (`git checkout -b feature/amazing-feature`)
3. 提交更改 (`git commit -m 'Add some amazing feature'`)
4. 推送到分支 (`git push origin feature/amazing-feature`)
5. 创建 Pull Request
贡献流程和 workflow 验证要求见 [`CONTRIBUTING.md`](CONTRIBUTING.md)。
-1
View File
@@ -76,5 +76,4 @@ class LightX2VDefaultConfig:
"audio_sr": 16000,
"talk_objects": None,
"boundary_step_index": 2,
"rope_type": "torch",
}
+40
View File
@@ -44,6 +44,39 @@ WRAPPER_TO_LIGHTX2V_FIELDS: Dict[str, str] = {
# Attention type fans out to three internal slots in lightx2v.
_ATTN_TYPE_SLOTS = ("self_attn_1_type", "cross_attn_1_type", "cross_attn_2_type")
# LightX2V unified the dedicated distilled Wan runners into the regular Wan
# runners in 23fc3d45. Keep old ComfyUI workflows loadable by translating the
# retired model class names to the new model class plus scheduler selector.
_LEGACY_MODEL_CLASS_ALIASES = {
"wan2.1_distill": ("wan2.1", "dmd2"),
"wan2.1_mean_flow_distill": ("wan2.1", "mean_flow"),
"wan2.2_moe_distill": ("wan2.2_moe", "dmd2"),
}
def _add_wan22_branch_paths(model_path: str, updates: Dict[str, Any]) -> None:
"""Expose Wan2.2's two transformer branches in the runner config.
Hugging Face model packages commonly keep the branches below
``distill_models/`` while the unified runner accepts explicit branch
directories. Supplying both path variants is harmless: the runner picks
the original or quantized key according to ``dit_quantized``.
"""
if not model_path:
return
for branch in ("high_noise_model", "low_noise_model"):
candidates = (
os.path.join(model_path, branch),
os.path.join(model_path, "distill_models", branch),
)
branch_path = next((path for path in candidates if os.path.isdir(path)), None)
if branch_path is None:
continue
config_key = branch.removesuffix("_model")
updates.setdefault(f"{config_key}_original_ckpt", branch_path)
updates.setdefault(f"{config_key}_quantized_ckpt", branch_path)
def apply_inference_config(config: Dict[str, Any]) -> Dict[str, Any]:
"""Translate inference widget values to a partial lightx2v config dict."""
@@ -65,9 +98,16 @@ def apply_inference_config(config: Dict[str, Any]) -> Dict[str, Any]:
# Wan2.2 MoE has two CFG scales (high/low noise) and a boundary param.
model_cls = config.get("model_cls", "")
legacy_model = _LEGACY_MODEL_CLASS_ALIASES.get(model_cls)
if legacy_model is not None:
model_cls, distill_method = legacy_model
updates["model_cls"] = model_cls
updates["distill_method"] = distill_method
if "wan2.2_moe" in model_cls:
updates["boundary"] = 0.9
updates["sample_guide_scale"] = [config.get("cfg_scale"), config.get("cfg_scale2")]
_add_wan22_branch_paths(config.get("model_path", ""), updates)
if "wan2.2" in model_cls:
updates["use_image_encoder"] = False
+1
View File
@@ -313,6 +313,7 @@ class ConfigBuilder:
("model_cls", None),
("model_path", None),
("task", None),
("distill_method", None),
("t5_quantized", False),
("clip_quantized", False),
("lora_configs", None),
+62 -91
View File
@@ -1,94 +1,65 @@
{
"1": {
"inputs": {
"video": "1954627330480766977_wan2-2.mp4",
"force_rate": 0,
"custom_width": 0,
"custom_height": 0,
"frame_load_cap": 0,
"skip_first_frames": 0,
"select_every_nth": 1,
"format": "AnimateDiff"
},
"class_type": "VHS_LoadVideo",
"_meta": {
"title": "Load Video (Upload) 🎥🅥🅗🅢"
}
"1": {
"inputs": {
"video": "飞书20260721-124954.mp4"
},
"4": {
"inputs": {
"ckpt_name": "seedvr2_ema_3b_fp8.safetensors",
"precision": "fp8-sgl",
"cpu_offload": false,
"use_tiling_vae": true
},
"class_type": "LightX2VSeedVR2Loader",
"_meta": {
"title": "LightX2V SeedVR2 Loader"
}
},
"5": {
"inputs": {
"target_height": 1920,
"target_width": 1080,
"infer_steps": 1,
"segment_length": 81,
"segment_overlap": 1,
"seed": 3816942657,
"source_fps": 16,
"model": [
"4",
0
],
"images": [
"1",
0
]
},
"class_type": "LightX2VSeedVR2Sampler",
"_meta": {
"title": "LightX2V SeedVR2 Sampler"
}
},
"6": {
"inputs": {
"video_info": [
"1",
3
]
},
"class_type": "VHS_VideoInfo",
"_meta": {
"title": "Video Info 🎥🅥🅗🅢"
}
},
"7": {
"inputs": {
"frame_rate": [
"6",
0
],
"loop_count": 0,
"filename_prefix": "AnimateDiff",
"format": "video/h264-mp4",
"pix_fmt": "yuv420p",
"crf": 19,
"save_metadata": true,
"trim_to_audio": false,
"pingpong": false,
"save_output": true,
"images": [
"5",
0
],
"audio": [
"1",
2
]
},
"class_type": "VHS_VideoCombine",
"_meta": {
"title": "Video Combine 🎥🅥🅗🅢"
}
"class_type": "LightX2VInputVideoPath",
"_meta": {
"title": "LightX2V Input Video Path (Upload)"
}
}
},
"2": {
"inputs": {
"ckpt_name": "seedvr2_ema_3b_fp8.safetensors",
"precision": "fp8-sgl",
"cpu_offload": false,
"use_tiling_vae": true,
"vae_tile_size": 1024,
"vae_tile_overlap": 64,
"vae_causal_slice_size": 4,
"vae_memory_limit_gb": 0.5
},
"class_type": "LightX2VSeedVR2Loader",
"_meta": {
"title": "LightX2V SeedVR2 Loader"
}
},
"3": {
"inputs": {
"model": [
"2",
0
],
"video_path": [
"1",
0
],
"target_width": 1080,
"target_height": 1920,
"infer_steps": 1,
"segment_length": 81,
"segment_overlap": 1,
"seed": 3816942657,
"filename_prefix": "lightx2v_seedvr2/SeedVR2",
"color_fix": "gpu"
},
"class_type": "LightX2VSeedVR2FileSampler",
"_meta": {
"title": "LightX2V SeedVR2 File Sampler"
}
},
"4": {
"inputs": {
"filename": [
"3",
0
],
"validate_exists": true,
"mux_audio": false
},
"class_type": "LightX2VOutputVideoPreview",
"_meta": {
"title": "LightX2V Output Video Preview"
}
}
}
+51 -42
View File
@@ -1,7 +1,7 @@
{
"11": {
"inputs": {
"audio": "12秒.mp3",
"audio": "audio_full_female_10s.wav",
"start_time": 0,
"duration": 0
},
@@ -25,6 +25,14 @@
"audio": [
"11",
0
],
"memory_config": [
"16",
0
],
"quantization_config": [
"17",
0
]
},
"class_type": "LightX2VConfigCombinerV2",
@@ -35,7 +43,7 @@
"14": {
"inputs": {
"model_cls": "seko_talk",
"model_name": "SekoTalk-v2.7_beta2-bf16-step4_temp",
"model_name": "SekoTalk-v2.7-fp8-step4-xt-490",
"task": "rs2v",
"infer_steps": 4,
"seed": 4221706066,
@@ -70,23 +78,54 @@
"title": "LightX2V Modular Inference V2"
}
},
"16": {
"inputs": {
"enable_rotary_chunk": false,
"rotary_chunk_size": 100,
"clean_cuda_cache": true,
"cpu_offload": true,
"offload_granularity": "block",
"offload_ratio": 1.0,
"t5_cpu_offload": true,
"t5_offload_granularity": "model",
"audio_encoder_cpu_offload": true,
"audio_adapter_cpu_offload": true,
"vae_cpu_offload": true,
"use_tiling_vae": true,
"lazy_load": false,
"unload_after_inference": false
},
"class_type": "LightX2VMemoryOptimization",
"_meta": {
"title": "LightX2V Memory Optimization"
}
},
"17": {
"inputs": {
"dit_quant_scheme": "fp8-sgl",
"t5_quant_scheme": "fp8-sgl",
"clip_quant_scheme": "fp8-sgl",
"adapter_quant_scheme": "fp8-sgl"
},
"class_type": "LightX2VQuantization",
"_meta": {
"title": "LightX2V Quantization"
}
},
"18": {
"inputs": {
"frame_rate": [
"22",
0
],
"frame_rate": 16,
"loop_count": 0,
"filename_prefix": "vigen-15ebb023",
"filename_prefix": "lightx2v_sekotalk/single",
"format": "video/h264-mp4",
"pix_fmt": "yuv420p",
"crf": 23,
"crf": 19,
"save_metadata": false,
"trim_to_audio": false,
"pingpong": false,
"save_output": false,
"save_output": true,
"images": [
"21",
"15",
0
],
"audio": [
@@ -101,41 +140,11 @@
},
"20": {
"inputs": {
"image": "00000.jpg"
"image": "codex_image_20260922_135831_1323a530_B.jpg"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
},
"21": {
"inputs": {
"source_fps": 16,
"target_fps": [
"22",
0
],
"scale": 1,
"model_name": "flownet.pkl",
"batch_size": 8,
"use_fp16": true,
"images": [
"15",
0
]
},
"class_type": "RIFEInterpolation",
"_meta": {
"title": "RIFE Frame Interpolation"
}
},
"22": {
"inputs": {
"value": 25.000000000000007
},
"class_type": "FloatConstant",
"_meta": {
"title": "target fps"
}
}
}
}
@@ -1,8 +1,8 @@
{
"id": "00000000-0000-0000-0000-000000000000",
"revision": 0,
"last_node_id": 50,
"last_link_id": 49,
"last_node_id": 51,
"last_link_id": 50,
"nodes": [
{
"id": 10,
@@ -32,16 +32,15 @@
"Node name for S&R": "LightX2VQuantization"
},
"widgets_values": [
"sgl",
"fp8",
"fp8",
"fp16",
"fp8"
"fp8-sgl",
"fp8-sgl",
"fp8-sgl",
"fp8-sgl"
]
},
{
"id": 11,
"type": "LightX2VConfigCombiner",
"type": "LightX2VConfigCombinerV3",
"pos": [
-1207.53271484375,
111.63562774658203
@@ -51,7 +50,7 @@
126
],
"flags": {},
"order": 14,
"order": 16,
"mode": 0,
"inputs": [
{
@@ -75,7 +74,7 @@
"name": "memory_config",
"shape": 7,
"type": "MEMORY_CONFIG",
"link": null
"link": 50
},
{
"name": "lora_chain",
@@ -88,41 +87,6 @@
"shape": 7,
"type": "TALK_OBJECTS_CONFIG",
"link": 49
}
],
"outputs": [
{
"name": "combined_config",
"type": "COMBINED_CONFIG",
"links": [
13
]
}
],
"properties": {
"Node name for S&R": "LightX2VConfigCombiner"
},
"widgets_values": []
},
{
"id": 12,
"type": "LightX2VModularInference",
"pos": [
-818.747802734375,
89.00883483886719
],
"size": [
400,
200
],
"flags": {},
"order": 16,
"mode": 0,
"inputs": [
{
"name": "combined_config",
"type": "COMBINED_CONFIG",
"link": 13
},
{
"name": "image",
@@ -134,7 +98,42 @@
"name": "audio",
"shape": 7,
"type": "AUDIO",
"link": 15
"link": null
}
],
"outputs": [
{
"name": "prepared_config",
"type": "PREPARED_CONFIG",
"links": [
13
]
}
],
"properties": {
"Node name for S&R": "LightX2VConfigCombinerV3"
},
"widgets_values": []
},
{
"id": 12,
"type": "LightX2VModularInferenceV2",
"pos": [
-818.747802734375,
89.00883483886719
],
"size": [
400,
200
],
"flags": {},
"order": 18,
"mode": 0,
"inputs": [
{
"name": "prepared_config",
"type": "PREPARED_CONFIG",
"link": 13
}
],
"outputs": [
@@ -154,7 +153,7 @@
}
],
"properties": {
"Node name for S&R": "LightX2VModularInference"
"Node name for S&R": "LightX2VModularInferenceV2"
},
"widgets_values": [
"The video feature the person is talking.",
@@ -195,44 +194,10 @@
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"input (1).png",
"codex_two_person_input.png",
"image"
]
},
{
"id": 20,
"type": "LoadAudio",
"pos": [
-1618.51171875,
526.3804931640625
],
"size": [
274.080078125,
136
],
"flags": {},
"order": 10,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "AUDIO",
"type": "AUDIO",
"links": [
15
]
}
],
"title": "加载音频",
"properties": {
"Node name for S&R": "LoadAudio"
},
"widgets_values": [
"15.wav",
null,
null
]
},
{
"id": 21,
"type": "VHS_VideoCombine",
@@ -337,8 +302,8 @@
},
"widgets_values": [
"seko_talk",
"Wan2.1-R2V0909-Audio-14B-720P-fp8",
"i2v",
"SekoTalk-v2.7-fp8-step4-xt-490",
"rs2v",
4,
584734195,
"randomize",
@@ -357,42 +322,6 @@
false
]
},
{
"id": 34,
"type": "easy showAnything",
"pos": [
-508.2369079589844,
-330.85296630859375
],
"size": [
511.8559875488281,
291.96728515625
],
"flags": {},
"order": 17,
"mode": 0,
"inputs": [
{
"name": "anything",
"shape": 7,
"type": "*",
"link": 42
}
],
"outputs": [
{
"name": "output",
"type": "*",
"links": []
}
],
"properties": {
"Node name for S&R": "easy showAnything"
},
"widgets_values": [
"TalkObjectsConfig(talk_objects=[{'name': 'person_1', 'audio': '/mnt/aigc/users/gaopeng1/ComfyUI/input/person_1_audio.wav', 'mask': '/mnt/aigc/users/gaopeng1/ComfyUI/input/person_1_mask.png', 'source_type': 'file'}, {'name': 'person_1', 'audio': '/mnt/aigc/users/gaopeng1/ComfyUI/input/person_1_audio.wav', 'mask': '/mnt/aigc/users/gaopeng1/ComfyUI/input/person_1_mask.png', 'source_type': 'file'}])"
]
},
{
"id": 36,
"type": "LoadAudio",
@@ -423,7 +352,7 @@
"Node name for S&R": "LoadAudio"
},
"widgets_values": [
"p2.mp3",
"audio_full_female_10s.wav",
null,
null
]
@@ -458,8 +387,8 @@
"Node name for S&R": "LoadImageMask"
},
"widgets_values": [
"input_p2_mask.png",
"red",
"codex_two_person_input.png",
"blue",
"image"
]
},
@@ -493,77 +422,7 @@
"Node name for S&R": "LoadImageMask"
},
"widgets_values": [
"input_p1_mask.png",
"red",
"image"
]
},
{
"id": 41,
"type": "LoadAudio",
"pos": [
-1468.0582275390625,
-386.4920654296875
],
"size": [
274.080078125,
136
],
"flags": {
"collapsed": true
},
"order": 4,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "AUDIO",
"type": "AUDIO",
"links": [
44
]
}
],
"properties": {
"Node name for S&R": "LoadAudio"
},
"widgets_values": [
"15.wav",
null,
null
]
},
{
"id": 42,
"type": "LoadImageMask",
"pos": [
-1477.0850830078125,
-332.2462463378906
],
"size": [
274.080078125,
318
],
"flags": {
"collapsed": false
},
"order": 3,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MASK",
"type": "MASK",
"links": [
45
]
}
],
"properties": {
"Node name for S&R": "LoadImageMask"
},
"widgets_values": [
"input_p3_mask.png",
"codex_two_person_input.png",
"red",
"image"
]
@@ -649,8 +508,7 @@
"name": "talk_objects_config",
"type": "TALK_OBJECTS_CONFIG",
"links": [
42,
48
49
]
}
],
@@ -744,50 +602,7 @@
"Node name for S&R": "LightX2VTalkObjectInput"
},
"widgets_values": [
"person_1",
true
]
},
{
"id": 46,
"type": "LightX2VTalkObjectInput",
"pos": [
-937.0840454101562,
-414.41925048828125
],
"size": [
317.0162048339844,
102
],
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "audio",
"shape": 7,
"type": "AUDIO",
"link": 44
},
{
"name": "mask",
"shape": 7,
"type": "MASK",
"link": 45
}
],
"outputs": [
{
"name": "talk_object",
"type": "TALK_OBJECT",
"links": []
}
],
"properties": {
"Node name for S&R": "LightX2VTalkObjectInput"
},
"widgets_values": [
"person_1",
"person_2",
true
]
},
@@ -821,59 +636,21 @@
"Node name for S&R": "LoadAudio"
},
"widgets_values": [
"p1.mp3",
"male_10s.wav",
null,
""
]
},
{
"id": 48,
"type": "SetNode",
"id": 51,
"type": "LightX2VMemoryOptimization",
"pos": [
-199.871337890625,
-578.7578735351562
-1750,
420
],
"size": [
210,
60
],
"flags": {},
"order": 18,
"mode": 0,
"inputs": [
{
"name": "TALK_OBJECTS_CONFIG",
"type": "TALK_OBJECTS_CONFIG",
"link": 48
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_talk_objects",
"properties": {
"previousName": "talk_objects"
},
"widgets_values": [
"talk_objects"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 50,
"type": "GetNode",
"pos": [
-1453.525390625,
249.66848754882812
],
"size": [
210,
60
290,
420
],
"flags": {},
"order": 7,
@@ -881,20 +658,32 @@
"inputs": [],
"outputs": [
{
"name": "TALK_OBJECTS_CONFIG",
"type": "TALK_OBJECTS_CONFIG",
"name": "memory_config",
"type": "MEMORY_CONFIG",
"links": [
49
50
]
}
],
"title": "Get_talk_objects",
"properties": {},
"properties": {
"Node name for S&R": "LightX2VMemoryOptimization"
},
"widgets_values": [
"talk_objects"
],
"color": "#232",
"bgcolor": "#353"
false,
100,
true,
true,
"block",
1.0,
true,
"model",
true,
true,
true,
true,
false,
false
]
}
],
"links": [
@@ -920,24 +709,16 @@
0,
12,
0,
"COMBINED_CONFIG"
"PREPARED_CONFIG"
],
[
14,
19,
0,
12,
1,
11,
6,
"IMAGE"
],
[
15,
20,
0,
12,
2,
"AUDIO"
],
[
18,
12,
@@ -994,30 +775,6 @@
1,
"TALK_OBJECT"
],
[
42,
43,
0,
34,
0,
"*"
],
[
44,
41,
0,
46,
0,
"AUDIO"
],
[
45,
42,
0,
46,
1,
"MASK"
],
[
47,
47,
@@ -1026,21 +783,21 @@
0,
"AUDIO"
],
[
48,
43,
0,
48,
0,
"*"
],
[
49,
50,
43,
0,
11,
5,
"TALK_OBJECTS_CONFIG"
],
[
50,
51,
0,
11,
3,
"MEMORY_CONFIG"
]
],
"groups": [
@@ -0,0 +1,150 @@
{
"11": {
"inputs": {
"audio": "audio_full_female_10s.wav",
"start_time": 0,
"duration": 0
},
"class_type": "VHS_LoadAudioUpload",
"_meta": {
"title": "Load Audio (Upload)🎥🅥🅗🅢"
}
},
"13": {
"inputs": {
"prompt": "A person speaks naturally to the camera, with clear lip movement and subtle facial expressions.",
"negative_prompt": "",
"inference_config": [
"14",
0
],
"image": [
"20",
0
],
"audio": [
"11",
0
],
"memory_config": [
"16",
0
],
"quantization_config": [
"17",
0
]
},
"class_type": "LightX2VConfigCombinerV2",
"_meta": {
"title": "LightX2V Config Combiner V2"
}
},
"14": {
"inputs": {
"model_cls": "seko_talk",
"model_name": "Wan2.1-R2V0909-Audio-14B-720P-fp8",
"task": "s2v",
"infer_steps": 4,
"seed": 4221706066,
"cfg_scale": 1,
"cfg_scale2": 1,
"sample_shift": 5,
"height": 1280,
"width": 720,
"duration": 5,
"attention_type": "sage_attn2",
"denoising_steps": "",
"resize_mode": "adaptive",
"fixed_area": "720p",
"segment_length": 81,
"prev_frame_length": 5,
"use_tiny_vae": false
},
"class_type": "LightX2VInferenceConfig",
"_meta": {
"title": "LightX2V Inference Config"
}
},
"15": {
"inputs": {
"prepared_config": [
"13",
0
]
},
"class_type": "LightX2VModularInferenceV2",
"_meta": {
"title": "LightX2V Modular Inference V2"
}
},
"16": {
"inputs": {
"enable_rotary_chunk": false,
"rotary_chunk_size": 100,
"clean_cuda_cache": true,
"cpu_offload": true,
"offload_granularity": "block",
"offload_ratio": 1.0,
"t5_cpu_offload": true,
"t5_offload_granularity": "model",
"audio_encoder_cpu_offload": true,
"audio_adapter_cpu_offload": true,
"vae_cpu_offload": true,
"use_tiling_vae": true,
"lazy_load": false,
"unload_after_inference": false
},
"class_type": "LightX2VMemoryOptimization",
"_meta": {
"title": "LightX2V Memory Optimization"
}
},
"17": {
"inputs": {
"dit_quant_scheme": "fp8-sgl",
"t5_quant_scheme": "fp8-sgl",
"clip_quant_scheme": "fp8-sgl",
"adapter_quant_scheme": "fp8-sgl"
},
"class_type": "LightX2VQuantization",
"_meta": {
"title": "LightX2V Quantization"
}
},
"18": {
"inputs": {
"frame_rate": 16,
"loop_count": 0,
"filename_prefix": "lightx2v_sekotalk/static",
"format": "video/h264-mp4",
"pix_fmt": "yuv420p",
"crf": 19,
"save_metadata": false,
"trim_to_audio": false,
"pingpong": false,
"save_output": true,
"images": [
"15",
0
],
"audio": [
"15",
1
]
},
"class_type": "VHS_VideoCombine",
"_meta": {
"title": "Video Combine 🎥🅥🅗🅢"
}
},
"20": {
"inputs": {
"image": "codex_image_20260922_135831_1323a530_B.jpg"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
}
}
@@ -202,4 +202,4 @@
"VHS_KeepIntermediate": true
},
"version": 0.4
}
}
+47 -230
View File
@@ -1,8 +1,8 @@
{
"id": "2ac26f51-8427-4fd5-a63b-8c0c35716909",
"revision": 0,
"last_node_id": 6,
"last_link_id": 10,
"last_node_id": 5,
"last_link_id": 11,
"nodes": [
{
"id": 2,
@@ -41,19 +41,50 @@
false
]
},
{
"id": 5,
"type": "LightX2VInputVideoPath",
"pos": [
-554.2465629475984,
172.59098534531842
],
"size": [
280,
120
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "video_path",
"type": "STRING",
"links": [
11
]
}
],
"properties": {
"Node name for S&R": "LightX2VInputVideoPath"
},
"widgets_values": [
"飞书20260721-124954.mp4"
]
},
{
"id": 3,
"type": "LightX2VSwiftVRSampler",
"type": "LightX2VSwiftVRFileSampler",
"pos": [
410.84880341840596,
119.34257282956952
],
"size": [
337.3125,
320
280
],
"flags": {},
"order": 3,
"order": 2,
"mode": 0,
"inputs": [
{
@@ -62,25 +93,12 @@
"link": 4
},
{
"name": "images",
"type": "IMAGE",
"link": 7
},
{
"name": "source_fps",
"type": "FLOAT",
"widget": {
"name": "source_fps"
},
"link": 9
"name": "video_path",
"type": "STRING",
"link": 11
}
],
"outputs": [
{
"name": "images",
"type": "IMAGE",
"links": null
},
{
"name": "filename",
"type": "STRING",
@@ -90,12 +108,10 @@
}
],
"properties": {
"Node name for S&R": "LightX2VSwiftVRSampler"
"Node name for S&R": "LightX2VSwiftVRFileSampler"
},
"widgets_values": [
1080,
16,
true,
"lightx2v_swiftvr_api/SwiftVR",
"libx265",
60,
@@ -114,19 +130,13 @@
703.328125
],
"flags": {},
"order": 4,
"order": 3,
"mode": 0,
"inputs": [
{
"name": "filename",
"type": "STRING",
"link": 6
},
{
"name": "audio",
"shape": 7,
"type": "AUDIO",
"link": 10
}
],
"outputs": [
@@ -141,173 +151,8 @@
},
"widgets_values": [
true,
true
false
]
},
{
"id": 5,
"type": "VHS_LoadVideo",
"pos": [
-554.2465629475984,
172.59098534531842
],
"size": [
260.421875,
761.6875
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "meta_batch",
"shape": 7,
"type": "VHS_BatchManager",
"link": null
},
{
"name": "vae",
"shape": 7,
"type": "VAE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
7
]
},
{
"name": "frame_count",
"type": "INT",
"links": null
},
{
"name": "audio",
"type": "AUDIO",
"links": [
10
]
},
{
"name": "video_info",
"type": "VHS_VIDEOINFO",
"links": [
8
]
}
],
"properties": {
"Node name for S&R": "VHS_LoadVideo"
},
"widgets_values": {
"video": "飞书20260721-124954.mp4",
"force_rate": 0,
"custom_width": 0,
"custom_height": 0,
"frame_load_cap": 0,
"skip_first_frames": 0,
"select_every_nth": 1,
"format": "AnimateDiff",
"videopreview": {
"hidden": false,
"paused": false,
"params": {
"filename": "飞书20260721-124954.mp4",
"type": "input",
"format": "video/mp4",
"force_rate": 0,
"custom_width": 0,
"custom_height": 0,
"frame_load_cap": 0,
"skip_first_frames": 0,
"select_every_nth": 1
}
}
}
},
{
"id": 6,
"type": "VHS_VideoInfo",
"pos": [
-245.90446161258,
309.43756146966683
],
"size": [
233.453125,
288
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "video_info",
"type": "VHS_VIDEOINFO",
"link": 8
}
],
"outputs": [
{
"name": "source_fps🟨",
"type": "FLOAT",
"links": [
9
]
},
{
"name": "source_frame_count🟨",
"type": "INT",
"links": null
},
{
"name": "source_duration🟨",
"type": "FLOAT",
"links": null
},
{
"name": "source_width🟨",
"type": "INT",
"links": null
},
{
"name": "source_height🟨",
"type": "INT",
"links": null
},
{
"name": "loaded_fps🟦",
"type": "FLOAT",
"links": null
},
{
"name": "loaded_frame_count🟦",
"type": "INT",
"links": null
},
{
"name": "loaded_duration🟦",
"type": "FLOAT",
"links": null
},
{
"name": "loaded_width🟦",
"type": "INT",
"links": null
},
{
"name": "loaded_height🟦",
"type": "INT",
"links": null
}
],
"properties": {
"Node name for S&R": "VHS_VideoInfo"
},
"widgets_values": {}
}
],
"links": [
@@ -322,42 +167,18 @@
[
6,
3,
1,
0,
4,
0,
"STRING"
],
[
7,
11,
5,
0,
3,
1,
"IMAGE"
],
[
8,
5,
3,
6,
0,
"VHS_VIDEOINFO"
],
[
9,
6,
0,
3,
2,
"FLOAT"
],
[
10,
5,
2,
4,
1,
"AUDIO"
"STRING"
]
],
"groups": [],
@@ -370,11 +191,7 @@
389.36680475087843
]
},
"frontendVersion": "1.45.15",
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,
"VHS_KeepIntermediate": true
"frontendVersion": "1.45.15"
},
"version": 0.4
}
}
-28
View File
@@ -142,34 +142,6 @@ class AudioFileHandler(FileHandler):
return waveform
def _save_with_wave(self, path: str, waveform: np.ndarray, sample_rate: int):
"""Save audio using wave module."""
import wave
with wave.open(path, "wb") as wav_file:
wav_file.setnchannels(1 if waveform.ndim == 1 else waveform.shape[-1])
wav_file.setsampwidth(2) # 16-bit
wav_file.setframerate(sample_rate)
if waveform.dtype != np.int16:
waveform = (waveform * 32767).astype(np.int16)
wav_file.writeframes(waveform.tobytes())
def _load_with_wave(self, path: str) -> Tuple[np.ndarray, int]:
"""Load audio using wave module."""
import wave
with wave.open(path, "rb") as wav_file:
sample_rate = wav_file.getframerate()
frames = wav_file.readframes(wav_file.getnframes())
waveform = np.frombuffer(frames, dtype=np.int16)
if wav_file.getnchannels() > 1:
waveform = waveform.reshape(-1, wav_file.getnchannels())
return waveform, sample_rate
class ImageFileHandler(FileHandler):
"""Handler for image files."""
+13 -5
View File
@@ -30,19 +30,27 @@ def scan_models() -> List[str]:
def support_model_cls_list() -> List[str]:
"""Return model classes understood by the bundled LightX2V revision.
The retired ``*_distill`` entries are kept as compatibility aliases for saved
ComfyUI workflows. The current LightX2V runner registry uses the base Wan
class plus ``distill_method`` instead; ``bridge.translator.inference``
canonicalizes those aliases before the runner is built.
"""
return [
"wan2.1",
"wan2.1_distill",
"wan2.1_vace",
"cogvideox",
"seko_talk",
"seko_talk_ar",
"wan2.2_moe",
"wan2.2",
"wan2.2_moe_audio",
"wan2.2_audio",
"wan2.2_moe_distill",
"wan2.2_s2v",
"qwen_image",
"seedvr2",
# Legacy workflow values; normalized before build_runner().
"wan2.1_distill",
"wan2.1_mean_flow_distill",
"wan2.2_moe_distill",
]
+13 -2
View File
@@ -41,6 +41,17 @@ _PATCH_SITES = (
)
class _AudioSentinel(str):
"""A path-shaped token that releases its registry lease when discarded."""
def __del__(self):
try:
release(str(self))
except Exception:
# Destructors can run during interpreter shutdown.
pass
def register(waveform: torch.Tensor, sample_rate: int) -> str:
"""Register a [C, T] float32 waveform; return a sentinel "path" for it."""
if waveform.dim() != 2:
@@ -50,12 +61,12 @@ def register(waveform: torch.Tensor, sample_rate: int) -> str:
stashed = waveform.detach().to(torch.float32).cpu().contiguous()
with _LOCK:
_REGISTRY[sentinel] = (stashed, int(sample_rate))
return sentinel
return _AudioSentinel(sentinel)
def release(sentinel: str) -> None:
with _LOCK:
_REGISTRY.pop(sentinel, None)
_REGISTRY.pop(str(sentinel), None)
def is_sentinel(path) -> bool:
+113
View File
@@ -0,0 +1,113 @@
"""Keep ComfyUI IMAGE inputs in memory while LightX2V reads them.
LightX2V's runner API names the request field ``image_path`` and accepts a
PIL image at the final read boundary. The wrapper used to serialize the first
ComfyUI frame to a temporary PNG only to have the runner decode it again. This
shim keeps the path-shaped request contract while replacing that disk
round-trip with a short-lived in-memory PIL image.
"""
from __future__ import annotations
import logging
import threading
import types
import uuid
from typing import Dict
import torch
from PIL import Image
SENTINEL_PREFIX = "<lightx2v-mem-image:"
SENTINEL_SUFFIX = ">"
_REGISTRY: Dict[str, Image.Image] = {}
_LOCK = threading.Lock()
class _ImageSentinel(str):
"""A path-shaped token that releases its registry lease when discarded.
The registry keeps a plain ``str`` key rather than this object, so the
object can be collected even while the registry still contains the image.
This covers failures between Combiner execution and ``generate()`` where
the normal inference ``finally`` block has not started yet.
"""
def __del__(self):
try:
release(str(self))
except Exception:
# Destructors can run during interpreter shutdown.
pass
def register(images: torch.Tensor) -> str:
"""Register the first ComfyUI IMAGE frame and return a path-shaped token."""
if not torch.is_tensor(images) or images.ndim != 4:
raise ValueError(f"IMAGE must be a [B, H, W, C] tensor; got {type(images).__name__} {getattr(images, 'shape', '')}")
if images.shape[-1] not in (1, 3, 4):
raise ValueError(f"IMAGE must have 1, 3, or 4 channels; got shape {tuple(images.shape)}")
frame = images[0].detach().cpu().float().clamp(0.0, 1.0)
frame_u8 = (frame.mul(255.0).round()).to(torch.uint8).numpy()
if frame_u8.shape[-1] == 1:
frame_u8 = frame_u8[..., 0]
elif frame_u8.shape[-1] == 4:
frame_u8 = frame_u8[..., :3]
token = f"{SENTINEL_PREFIX}{uuid.uuid4().hex}{SENTINEL_SUFFIX}"
image = Image.fromarray(frame_u8)
if image.mode != "RGB":
image = image.convert("RGB")
with _LOCK:
_REGISTRY[token] = image
return _ImageSentinel(token)
def release(sentinel: str | None) -> None:
if not is_sentinel(sentinel):
return
with _LOCK:
_REGISTRY.pop(str(sentinel), None)
def is_sentinel(path) -> bool:
return isinstance(path, str) and path.startswith(SENTINEL_PREFIX)
def _lookup(sentinel: str) -> Image.Image:
with _LOCK:
image = _REGISTRY.get(sentinel)
if image is None:
raise FileNotFoundError(f"Stale LightX2V in-memory image sentinel: {sentinel}")
return image
def install(runner) -> bool:
"""Patch one runner's image read boundary; return whether it was patched."""
if runner is None or not hasattr(runner, "read_image_input"):
return False
if hasattr(runner, "_lightx2v_original_read_image_input"):
return True
original = runner.read_image_input
def _read_image_input(self, image_path):
if is_sentinel(image_path):
image_path = _lookup(image_path)
return original(image_path)
runner._lightx2v_original_read_image_input = original
runner.read_image_input = types.MethodType(_read_image_input, runner)
logging.info("_image_shim installed on %s", type(runner).__name__)
return True
def install_on_runner_tree(runner) -> int:
"""Patch a regular runner or every clip runner inside an rs2v pipeline."""
targets = [runner]
clip_generators = getattr(runner, "clip_generators", None)
if isinstance(clip_generators, dict):
targets.extend(clip_generators.values())
return sum(1 for target in targets if install(target))
+53
View File
@@ -0,0 +1,53 @@
"""Small adapters for LightX2V 0.6's startup/request runner API."""
from contextlib import contextmanager
def build_runner(startup_data):
"""Build a resident LightX2V runner from deployment-time settings."""
from ..lightx2v.lightx2v.models.runners.runner_factory import build_runner as _build_runner
from ..lightx2v.lightx2v.utils.set_config import build_startup_config
startup_config = build_startup_config(dict(startup_data))
return _build_runner(startup_config)
@contextmanager
def temporary_runner_config(runner, values):
"""Apply per-call implementation settings while a request is running.
LightX2V 0.6 keeps deployment settings in a locked ``LockableDict`` and
exposes request data through ``prepare_request``. A few legacy ComfyUI
controls (SeedVR segmenting/color correction and SwiftVR video encoding)
are still implementation settings, so they need a narrow, serialized
override instead of being passed as unsupported request fields.
"""
values = {key: value for key, value in values.items() if value is not None}
if not values:
yield
return
config = runner.config
missing = object()
previous = {key: config.get(key, missing) for key in values}
unlock = getattr(config, "temporarily_unlocked", None)
if unlock is None:
raise RuntimeError("LightX2V runner config does not support temporary request settings")
with unlock():
config.update(values)
try:
yield
finally:
for key, value in previous.items():
if value is missing:
config.pop(key, None)
else:
config[key] = value
def run_request(runner, request, *, runtime_config=None):
"""Validate one modern request and execute it through the runner API."""
with temporary_runner_config(runner, runtime_config or {}):
input_info = runner.prepare_request(dict(request))
return runner.run_request(input_info)
+12 -7
View File
@@ -168,19 +168,24 @@ class _BaseConfigCombiner:
if image is None:
raise ValueError("i2v or s2v or rs2v task requires input image")
image_np = (image[0].cpu().numpy() * 255).astype(np.uint8)
pil_image = Image.fromarray(image_np)
temp_path = self.temp_manager.create_temp_file(suffix=".png")
pil_image.save(temp_path)
config.image_path = temp_path
logging.info(f"Image saved to {temp_path}")
frame_count = int(image.shape[0]) if hasattr(image, "shape") and image.ndim > 0 else 0
if frame_count > 1:
logging.warning(
"LightX2V uses only the first IMAGE frame as the reference; received %s frames",
frame_count,
)
from ._image_shim import register
config.image_path = register(image)
logging.info("Routed ComfyUI IMAGE through the in-memory shim")
def _save_single_audio_if_needed(self, config, audio):
# Route ComfyUI AUDIO straight into the runner via the in-memory shim
# (no WAV temp file, no soundfile round-trip). The shim only intercepts
# this single-AUDIO path; V3 multi-talker padding still produces real
# files since its inputs are external paths/URLs, not ComfyUI tensors.
if audio is None or not hasattr(config, "model_cls") or "seko" not in config.model_cls:
if audio is None or config.task not in {"s2v", "rs2v"}:
return
from ._audio_shim import comfyui_audio_to_loader_pair, install, register
+3 -3
View File
@@ -42,7 +42,7 @@ class LightX2VInferenceConfig:
["t2v", "i2v", "s2v", "rs2v"],
{
"default": "i2v",
"tooltip": "Task type: text-to-video or image-to-video or reference_image and audio to video (shot)",
"tooltip": "Task: t2v text-to-video, i2v image-to-video, s2v image+audio-to-video, or rs2v reference-shot image+audio-to-video",
},
),
"infer_steps": (
@@ -136,7 +136,7 @@ class LightX2VInferenceConfig:
],
{
"default": "adaptive",
"tooltip": "Adaptive resize input image to target aspect ratio",
"tooltip": "Resize policy: adaptive preserves the target area, fixed_shape uses the exact target size, and fixed-area/min-side modes preserve aspect ratio",
},
),
"fixed_area": (
@@ -161,7 +161,7 @@ class LightX2VInferenceConfig:
"default": 5,
"min": 0,
"max": 16,
"tooltip": "Previous frame overlap for sekotalk models",
"tooltip": "Previous-frame overlap for rs2v continuation; s2v uses the model's own initial reference and usually ignores this value",
},
),
"use_tiny_vae": (
+150 -31
View File
@@ -7,9 +7,36 @@ import torch
from comfy.utils import ProgressBar
from ..config_builder import ConfigBuilder
from ..lightx2v.lightx2v.infer import init_runner
from ..lightx2v.lightx2v.utils.input_info import init_empty_input_info, update_input_info_from_dict
from ..lightx2v.lightx2v.utils.set_config import auto_calc_config, set_args2config
# LightX2V 0.6 replaced the resident ``init_runner`` entrypoint and the
# ``set_args2config``/``auto_calc_config`` pair with a startup/request split.
# Keep the imports optional so the wrapper can select the API at runtime while
# still supporting the previous submodule revision.
try:
from ..lightx2v.lightx2v.infer import init_runner as _legacy_init_runner
except ImportError:
_legacy_init_runner = None
try:
from ..lightx2v.lightx2v.utils.input_info import init_empty_input_info as _legacy_init_empty_input_info
from ..lightx2v.lightx2v.utils.input_info import update_input_info_from_dict as _legacy_update_input_info_from_dict
except ImportError:
_legacy_init_empty_input_info = None
_legacy_update_input_info_from_dict = None
try:
from ..lightx2v.lightx2v.utils.set_config import auto_calc_config as _legacy_auto_calc_config
from ..lightx2v.lightx2v.utils.set_config import set_args2config as _legacy_set_args2config
except ImportError:
_legacy_auto_calc_config = None
_legacy_set_args2config = None
try:
from ..lightx2v.lightx2v.models.runners.runner_factory import build_runner as _modern_build_runner
from ..lightx2v.lightx2v.utils.set_config import build_startup_config as _modern_build_startup_config
except ImportError:
_modern_build_runner = None
_modern_build_startup_config = None
class LightX2VModularInferenceV2:
@@ -62,16 +89,31 @@ class LightX2VModularInferenceV2:
# never get merged — rs2v_infer then KeyErrors on config["vae_stride"].
if "task" not in config:
config["task"] = "rs2v"
# set_config = set_args2config + auto_calc_config. set_args2config strips
# any key that's part of an InputInfo dataclass (target_video_length,
# infer_steps, seed, ...). For CLI runs auto_calc_config recovers them
# by merging --config_json, but we have no external JSON, so
# auto_calc_config's `config["target_video_length"]` access KeyErrors.
# Inject the bridge between set_args2config and auto_calc_config.
target_video_length = config.get("target_video_length", config.get("segment_length", config.get("video_length", 81)))
formatted = set_args2config(config)
formatted["target_video_length"] = target_video_length
formatted = auto_calc_config(formatted)
# LightX2V 0.6 uses the same startup builder as the regular runner;
# older revisions need the set_args2config + auto_calc_config pair.
# The shot runner's load_clip_configs() calls build_startup_config
# again, so passing the modern startup shape here is safe and keeps
# model config/vae metadata loading in LightX2V itself.
if _modern_build_startup_config is not None:
startup_data = dict(config)
startup_data["size"] = [
int(config.get("target_height", config.get("height", 480))),
int(config.get("target_width", config.get("width", 832))),
]
startup_data["num_frames"] = int(config.get("target_video_length", config.get("video_length", config.get("num_frames", 81))))
formatted = _modern_build_startup_config(startup_data)
else:
# set_args2config strips any key that's part of an InputInfo
# dataclass (target_video_length, infer_steps, seed, ...). For
# CLI runs auto_calc_config recovers them by merging
# --config_json; our in-memory flow has no external JSON, so
# bridge target_video_length back before auto_calc_config.
target_video_length = config.get("target_video_length", config.get("segment_length", config.get("video_length", 81)))
if _legacy_set_args2config is None or _legacy_auto_calc_config is None:
raise RuntimeError("No compatible LightX2V shot-runner config API found")
formatted = _legacy_set_args2config(config)
formatted["target_video_length"] = target_video_length
formatted = _legacy_auto_calc_config(formatted)
main_cfg = {
"lightx2v_path": "",
"clip_configs": [
@@ -88,10 +130,61 @@ class LightX2VModularInferenceV2:
return load_clip_configs(main_cfg)
@staticmethod
def _build_modern_startup_config(config):
"""Translate wrapper shape fields into LightX2V 0.6 startup fields."""
if _modern_build_startup_config is None or _modern_build_runner is None:
raise RuntimeError("The installed LightX2V revision does not provide the modern startup API")
startup_data = dict(config)
startup_data["size"] = [
int(config.get("target_height", config.get("height", 480))),
int(config.get("target_width", config.get("width", 832))),
]
startup_data["num_frames"] = int(config.get("target_video_length", config.get("video_length", config.get("num_frames", 81))))
return _modern_build_startup_config(startup_data)
@staticmethod
def _build_modern_request(config, runner):
"""Build only fields accepted by the current runner's request schema."""
task = config.get("task")
supported_fields = runner.get_supported_request_fields(task)
request = {}
for field in supported_fields:
if field == "size":
# SekoTalk's fixed-area/min-side modes derive the output size
# from the model's resize policy. The 0.6 request validator
# rejects an explicit size unless the request selected
# fixed_shape; keep the wrapper's target dimensions in startup
# config but do not send them as a per-request override.
if config.get("model_cls", "").startswith("seko") and config.get("resize_mode") != "fixed_shape":
continue
request[field] = [
int(config.get("target_height", config.get("height", 480))),
int(config.get("target_width", config.get("width", 832))),
]
elif field == "num_frames":
request[field] = int(config.get("target_video_length", config.get("video_length", config.get("num_frames", 81))))
elif field in config:
request[field] = config[field]
request["task"] = task
request["return_result_tensor"] = True
request["save_result_path"] = ""
return request
def generate(self, prepared_config):
"""Run inference with prepared configuration."""
config = prepared_config
from ._image_shim import install_on_runner_tree as _install_image_shim
from ._image_shim import is_sentinel as _is_image_sentinel
from ._image_shim import release as _release_image_sentinel
_image_sentinel = config.get("image_path") if isinstance(config, dict) else getattr(config, "image_path", None)
if not _is_image_sentinel(_image_sentinel):
_image_sentinel = None
# Combiner may have stashed the input AUDIO in the in-memory shim and
# set audio_path to a sentinel — release it on exit so the tensor isn't
# retained across runs (one ComfyUI graph tick = one sentinel).
@@ -108,7 +201,7 @@ class LightX2VModularInferenceV2:
current_runner = getattr(self.__class__, "_current_runner", None)
current_config_hash = getattr(self.__class__, "_current_config_hash", None)
needs_reinit = current_runner is None or current_config_hash != config_hash or getattr(config, "lazy_load", False)
needs_reinit = current_runner is None or current_config_hash != config_hash or config.get("lazy_load", False)
logging.info(f"Needs reinit: {needs_reinit}, old config hash: {current_config_hash}, new config hash: {config_hash}")
if needs_reinit:
@@ -124,19 +217,22 @@ class LightX2VModularInferenceV2:
shot_cfg = self._build_rs2v_shot_config(config)
self.__class__._current_runner = ShotRS2VPipeline(shot_cfg)
else:
# set_args2config strips InputInfo-dataclass keys (target_video_length,
# infer_steps, ...). CLI flows recover them via --config_json merging
# inside auto_calc_config; our in-memory flow has no external JSON, so
# we bridge target_video_length manually between the two halves so
# auto_calc_config:194's modulo check on s2v/i2v doesn't KeyError.
target_video_length = config.get(
"target_video_length",
config.get("segment_length", config.get("video_length", 81)),
)
formatted_config = set_args2config(config)
formatted_config["target_video_length"] = target_video_length
formatted_config = auto_calc_config(formatted_config)
self.__class__._current_runner = init_runner(formatted_config)
if _modern_build_runner is not None and _modern_build_startup_config is not None:
startup_config = self._build_modern_startup_config(config)
self.__class__._current_runner = _modern_build_runner(startup_config)
else:
if _legacy_set_args2config is None or _legacy_auto_calc_config is None or _legacy_init_runner is None:
raise RuntimeError("No compatible LightX2V runner API found in the installed LightX2V submodule")
# The legacy API strips InputInfo-dataclass keys. Bridge
# target_video_length back before auto_calc_config.
target_video_length = config.get(
"target_video_length",
config.get("segment_length", config.get("video_length", 81)),
)
formatted_config = _legacy_set_args2config(config)
formatted_config["target_video_length"] = target_video_length
formatted_config = _legacy_auto_calc_config(formatted_config)
self.__class__._current_runner = _legacy_init_runner(formatted_config)
self.__class__._current_config_hash = config_hash
progress = ProgressBar(100)
@@ -146,6 +242,10 @@ class LightX2VModularInferenceV2:
current_runner = self.__class__._current_runner
if _image_sentinel is not None:
if _install_image_shim(current_runner) == 0:
raise RuntimeError("LightX2V IMAGE input shim could not find a runner image reader")
if hasattr(current_runner, "set_progress_callback"):
current_runner.set_progress_callback(update_progress)
@@ -153,10 +253,27 @@ class LightX2VModularInferenceV2:
config["save_result_path"] = ""
config["negative_prompt"] = config.get("negative_prompt", "")
if config.get("task") == "rs2v":
result_dict = current_runner.run_pipeline(config)
# ShotRS2VPipeline delegates request validation to its clip
# runner. Passing the prepared config wholesale worked with
# the old InputInfo path, but LightX2V 0.6 rejects startup
# fields (model paths, quantization flags, etc.) as request
# fields. Filter the request against the modern runner schema
# before handing it to the shot pipeline.
if _modern_build_runner is not None and _modern_build_startup_config is not None:
shot_runner = current_runner.clip_generators["rs2v_clip"]
request_data = self._build_modern_request(config, shot_runner)
result_dict = current_runner.run_pipeline(request_data)
else:
result_dict = current_runner.run_pipeline(config)
elif _modern_build_runner is not None and _modern_build_startup_config is not None:
request_data = self._build_modern_request(config, current_runner)
input_info = current_runner.prepare_request(request_data)
result_dict = current_runner.run_request(input_info)
else:
input_data = init_empty_input_info(config.task)
update_input_info_from_dict(input_data, config)
if _legacy_init_empty_input_info is None or _legacy_update_input_info_from_dict is None:
raise RuntimeError("No compatible LightX2V input-info API found in the installed LightX2V submodule")
input_data = _legacy_init_empty_input_info(config.task)
_legacy_update_input_info_from_dict(input_data, config)
current_runner.set_config(config)
result_dict = current_runner.run_pipeline(input_data)
@@ -168,7 +285,7 @@ class LightX2VModularInferenceV2:
if images.dtype != torch.float32:
images = images.float()
if getattr(config, "unload_after_inference", False):
if config.get("unload_after_inference", False):
current_runner = None # drop local ref so __del__ can run
self._release_runner()
else:
@@ -181,5 +298,7 @@ class LightX2VModularInferenceV2:
logging.error(f"Error during inference: {e}")
raise
finally:
if _image_sentinel is not None:
_release_image_sentinel(_image_sentinel)
if _audio_sentinel is not None:
_release_audio_sentinel(_audio_sentinel)
+46 -86
View File
@@ -13,7 +13,6 @@ IMAGE tensor (no temp file, no re-encode); the runner's segmenting logic still
runs and slices our in-memory tensor.
"""
import argparse
import gc
import logging
import math
@@ -30,6 +29,7 @@ import folder_paths
import torch
from comfy.utils import ProgressBar
from ._lightx2v_runner import build_runner, run_request
from .file_input import probe_video_file, resolve_input_video_path
logger = logging.getLogger(__name__)
@@ -209,7 +209,7 @@ def _install_tensor_input_shim(runner, frames_u8, fps):
def _probe_video(self, video_path): # noqa: ARG001
total = self._tensor_input.shape[0]
self._set_output_fps(self._tensor_input_fps)
self.set_output_fps(self._tensor_input_fps)
return total, self._tensor_input_fps, None
def _read_video_segment(self, video_path, start_idx, end_idx): # noqa: ARG001
@@ -330,9 +330,6 @@ class LightX2VSeedVR2Loader:
vae_causal_slice_size,
vae_memory_limit_gb,
):
from ..lightx2v.lightx2v.infer import init_runner
from ..lightx2v.lightx2v.utils.set_config import set_config
model_dir = _seedvr2_model_dir()
if ckpt_name == "None":
raise FileNotFoundError(f"No .safetensors checkpoints found in {model_dir}")
@@ -376,8 +373,7 @@ class LightX2VSeedVR2Loader:
else:
config["dit_original_ckpt"] = str(ckpt_path)
formatted = set_config(argparse.Namespace(**config))
runner = init_runner(formatted)
runner = build_runner(config)
logger.info(
"[SeedVR2Loader] loaded %s (%s); cpu_offload=%s, tile_vae=%s, tile=%s, overlap=%s, slice=%s, mem_limit=%sGiB",
ckpt_name,
@@ -467,10 +463,11 @@ class LightX2VSeedVR2Sampler:
filename_prefix,
color_fix,
):
from ..lightx2v.lightx2v.utils.input_info import init_empty_input_info, update_input_info_from_dict
runner = model["runner"]
if int(infer_steps) != 1:
raise ValueError("SeedVR2 requires infer_steps=1")
if images.dim() != 4 or images.shape[-1] not in (3, 4):
raise ValueError(f"Expected IMAGE [T, H, W, C], got shape {tuple(images.shape)}")
# ComfyUI IMAGE: [T, H, W, C] float[0,1] → [T, C, H, W] uint8 (read_video's contract)
@@ -496,49 +493,31 @@ class LightX2VSeedVR2Sampler:
full_path, output_file, output_subfolder = _prepare_output_video(filename_prefix, target_width, target_height)
save_path = str(full_path)
# runner.config is a LockableDict (locked after init); set_config uses temporarily_unlocked.
runner.set_config(
{
"sr_ratio": float(sr_ratio),
"target_height": int(target_height),
"target_width": int(target_width),
"target_video_length": int(segment_length), # vestigial for SR; keep aligned with segment_length
"sr_segment_length": int(segment_length),
"sr_overlap": int(segment_overlap),
"infer_steps": int(infer_steps),
"seed": int(seed),
"fps": float(source_fps),
"video_path": "<tensor>", # truthy sentinel so segmenting logic runs; shim bypasses file I/O
"image_path": "",
"prompt": "",
"negative_prompt": "",
"save_result_path": save_path,
"return_result_tensor": not bool(save_to_output_file),
"color_fix": str(color_fix),
}
)
input_info = init_empty_input_info("sr")
update_input_info_from_dict(
input_info,
{
"video_path": "<tensor>",
"image_path": "",
"prompt": "",
"negative_prompt": "",
"seed": int(seed),
"sr_ratio": float(sr_ratio),
"save_result_path": save_path,
"return_result_tensor": not bool(save_to_output_file),
},
)
request = {
"task": "sr",
"video_path": "<tensor>",
"image_path": "",
"size": [int(target_height), int(target_width)],
"seed": int(seed),
"sr_ratio": float(sr_ratio),
"save_result_path": save_path,
"return_result_tensor": not bool(save_to_output_file),
}
runtime_config = {
"sr_segment_length": int(segment_length),
"sr_overlap": int(segment_overlap),
"fps": float(source_fps),
"color_fix": str(color_fix),
"stream_save_video": bool(save_to_output_file),
}
progress = ProgressBar(100)
if hasattr(runner, "set_progress_callback"):
runner.set_progress_callback(lambda cur, _tot: progress.update_absolute(cur))
try:
result = runner.run_pipeline(input_info)
with _SEEDVR_RUN_LOCK:
result = run_request(runner, request, runtime_config=runtime_config)
finally:
_clear_tensor_input_shim(runner)
torch.cuda.empty_cache()
@@ -628,8 +607,6 @@ class LightX2VSeedVR2FileSampler:
filename_prefix,
color_fix,
):
from ..lightx2v.lightx2v.utils.input_info import init_empty_input_info, update_input_info_from_dict
input_path = resolve_input_video_path(video_path)
source_width, source_height, source_fps = probe_video_file(input_path)
effective_fps = source_fps if source_fps > 0 else 16.0
@@ -647,20 +624,25 @@ class LightX2VSeedVR2FileSampler:
full_path, output_file, output_subfolder = _prepare_output_video(filename_prefix, target_width, target_height)
save_path = str(full_path)
input_info = init_empty_input_info("sr")
update_input_info_from_dict(
input_info,
{
"video_path": str(input_path),
"image_path": "",
"prompt": "",
"negative_prompt": "",
"seed": int(seed),
"sr_ratio": float(sr_ratio),
"save_result_path": save_path,
"return_result_tensor": False,
},
)
if int(infer_steps) != 1:
raise ValueError("SeedVR2 requires infer_steps=1")
request = {
"task": "sr",
"video_path": str(input_path),
"image_path": "",
"size": [int(target_height), int(target_width)],
"seed": int(seed),
"sr_ratio": float(sr_ratio),
"save_result_path": save_path,
"return_result_tensor": False,
}
runtime_config = {
"sr_segment_length": int(segment_length),
"sr_overlap": int(segment_overlap),
"stream_save_video": True,
"fps": float(effective_fps),
"color_fix": str(color_fix),
}
runner = model["runner"]
progress = ProgressBar(100)
@@ -677,31 +659,9 @@ class LightX2VSeedVR2FileSampler:
)
try:
with _SEEDVR_RUN_LOCK:
_clear_tensor_input_shim(runner)
runner.set_config(
{
"sr_ratio": float(sr_ratio),
"target_height": int(target_height),
"target_width": int(target_width),
"target_video_length": int(segment_length),
"sr_segment_length": int(segment_length),
"sr_overlap": int(segment_overlap),
"stream_save_video": True,
"infer_steps": int(infer_steps),
"seed": int(seed),
"fps": float(effective_fps),
"video_path": str(input_path),
"image_path": "",
"prompt": "",
"negative_prompt": "",
"save_result_path": save_path,
"return_result_tensor": False,
"color_fix": str(color_fix),
}
)
if hasattr(runner, "set_progress_callback"):
runner.set_progress_callback(lambda current, _total: progress.update_absolute(current))
runner.run_pipeline(input_info)
run_request(runner, request, runtime_config=runtime_config)
finally:
torch.cuda.empty_cache()
gc.collect()
@@ -741,7 +701,7 @@ class LightX2VOutputVideoPreview:
"AUDIO",
{"tooltip": "Optional audio from Load Video/Get Video Components to merge into the output MP4."},
),
}
},
}
RETURN_TYPES = ("STRING",)
+37 -70
View File
@@ -1,6 +1,5 @@
"""SwiftVR restoration nodes for ComfyUI."""
import argparse
import gc
import logging
import tempfile
@@ -12,6 +11,7 @@ import folder_paths
import torch
from comfy.utils import ProgressBar
from ._lightx2v_runner import build_runner, run_request
from .file_input import probe_video_file, resolve_input_video_path
from .seedvr import _prepare_output_video
@@ -81,10 +81,7 @@ def _resolve_output_size(source_width: int, source_height: int, target_short_edg
scale = target_short_edge / source_short_edge
if scale > _MAX_SR_RATIO:
raise ValueError(
f"SwiftVR scale {scale:.3f}x exceeds the supported maximum {_MAX_SR_RATIO:.1f}x; "
f"lower target_short_edge"
)
raise ValueError(f"SwiftVR scale {scale:.3f}x exceeds the supported maximum {_MAX_SR_RATIO:.1f}x; lower target_short_edge")
if source_width <= source_height:
output_width = target_short_edge
output_height = int(round(source_height * scale))
@@ -97,8 +94,7 @@ def _resolve_output_size(source_width: int, source_height: int, target_short_edg
output_height = max(2, (output_height + 1) // 2 * 2)
if max(output_width, output_height) > _MAX_OUTPUT_DIMENSION:
raise ValueError(
f"SwiftVR output {output_width}x{output_height} exceeds the maximum supported dimension "
f"{_MAX_OUTPUT_DIMENSION}; lower target_short_edge"
f"SwiftVR output {output_width}x{output_height} exceeds the maximum supported dimension {_MAX_OUTPUT_DIMENSION}; lower target_short_edge"
)
return output_height, output_width, scale
@@ -189,9 +185,6 @@ class LightX2VSwiftVRLoader:
CATEGORY = "LightX2V/SwiftVR"
def load(self, model_name, attention_backend, rope_type, clip_length, dit_overlap, reae_frame_batch_size, use_compile):
from ..lightx2v.lightx2v.infer import init_runner
from ..lightx2v.lightx2v.utils.set_config import set_config
model_path = _swiftvr_model_root() / model_name
if model_name == "None" or not _is_swiftvr_model(model_path):
missing = [str(model_path / relative_path) for relative_path in _REQUIRED_MODEL_FILES if not (model_path / relative_path).is_file()]
@@ -218,7 +211,7 @@ class LightX2VSwiftVRLoader:
"parallel": False,
"use_compile": bool(use_compile),
}
runner = init_runner(set_config(argparse.Namespace(**config)))
runner = build_runner(config)
logger.info(
"[SwiftVRLoader] loaded %s; attention=%s, rope=%s, clip=%s, overlap=%s, reae_batch=%s, compile=%s",
model_name,
@@ -288,8 +281,6 @@ class LightX2VSwiftVRSampler:
):
from lightx2v.models.runners.swiftvr import swiftvr_runner as swiftvr_module
from ..lightx2v.lightx2v.utils.input_info import init_empty_input_info, update_input_info_from_dict
if images.dim() != 4 or images.shape[-1] not in (3, 4):
raise ValueError(f"Expected IMAGE [T, H, W, C], got shape {tuple(images.shape)}")
if images.shape[0] < 1:
@@ -308,7 +299,7 @@ class LightX2VSwiftVRSampler:
target_short_edge,
require_even=not is_image,
)
target_shape = [output_height, output_width]
target_size = [output_height, output_width]
logger.info(
"[SwiftVRSampler] aligned input=%sx%s, target_short_edge=%s, output=%sx%s, scale=%.4f",
source_width,
@@ -340,18 +331,21 @@ class LightX2VSwiftVRSampler:
video_path = "" if is_image else "<tensor>"
return_result_tensor = is_image
input_info = init_empty_input_info("sr")
update_input_info_from_dict(
input_info,
{
"video_path": video_path,
"image_path": image_path,
"sr_ratio": float(sr_ratio),
"target_shape": target_shape,
"save_result_path": save_path,
"return_result_tensor": return_result_tensor,
},
)
request = {
"task": "sr",
"video_path": video_path,
"image_path": image_path,
"sr_ratio": float(sr_ratio),
"size": target_size,
"save_result_path": save_path,
"return_result_tensor": return_result_tensor,
}
runtime_config = {
"fps": float(source_fps),
"video_codec": str(video_codec),
"quality": int(quality),
"ffmpeg_preset": str(ffmpeg_preset),
}
progress = ProgressBar(100)
restored_images = None
@@ -364,19 +358,6 @@ class LightX2VSwiftVRSampler:
had_instance_image_reader = "read_image_frame" in runner.__dict__
original_instance_image_reader = runner.__dict__.get("read_image_frame")
try:
runner.set_config(
{
"fps": float(source_fps),
"video_codec": str(video_codec),
"quality": int(quality),
"ffmpeg_preset": str(ffmpeg_preset),
"video_path": video_path,
"image_path": image_path,
"sr_ratio": float(sr_ratio),
"target_shape": target_shape,
"return_result_tensor": return_result_tensor,
}
)
if hasattr(runner, "set_progress_callback"):
runner.set_progress_callback(lambda current, _total: progress.update_absolute(current))
if is_image:
@@ -390,7 +371,7 @@ class LightX2VSwiftVRSampler:
swiftvr_module.mux_audio_from_video = lambda *_args, **_kwargs: None
if not is_image and memory_writer is not None:
runner.open_video_writer = types.MethodType(lambda _runner, _path, _fps: memory_writer, runner)
result = runner.run_pipeline(input_info)
result = run_request(runner, request, runtime_config=runtime_config)
if is_image:
restored_images = result.get("images") if isinstance(result, dict) else result
finally:
@@ -474,8 +455,6 @@ class LightX2VSwiftVRFileSampler:
CATEGORY = "LightX2V/SwiftVR"
def sample(self, model, video_path, target_short_edge, filename_prefix, video_codec, quality, ffmpeg_preset):
from ..lightx2v.lightx2v.utils.input_info import init_empty_input_info, update_input_info_from_dict
input_path = resolve_input_video_path(video_path)
raw_width, raw_height, source_fps = probe_video_file(input_path)
source_height, source_width = raw_height // 8 * 8, raw_width // 8 * 8
@@ -490,20 +469,21 @@ class LightX2VSwiftVRFileSampler:
)
full_path, output_file, output_subfolder = _prepare_output_video(filename_prefix, output_width, output_height)
save_path = str(full_path)
target_shape = [output_height, output_width]
input_info = init_empty_input_info("sr")
update_input_info_from_dict(
input_info,
{
"video_path": str(input_path),
"image_path": "",
"sr_ratio": float(sr_ratio),
"target_shape": target_shape,
"save_result_path": save_path,
"return_result_tensor": False,
},
)
request = {
"task": "sr",
"video_path": str(input_path),
"image_path": "",
"sr_ratio": float(sr_ratio),
"size": [output_height, output_width],
"save_result_path": save_path,
"return_result_tensor": False,
}
runtime_config = {
"fps": 0.0,
"video_codec": str(video_codec),
"quality": int(quality),
"ffmpeg_preset": str(ffmpeg_preset),
}
runner = model["runner"]
progress = ProgressBar(100)
@@ -518,22 +498,9 @@ class LightX2VSwiftVRFileSampler:
)
try:
with _SWIFTVR_RUN_LOCK:
runner.set_config(
{
"fps": 0.0,
"video_codec": str(video_codec),
"quality": int(quality),
"ffmpeg_preset": str(ffmpeg_preset),
"video_path": str(input_path),
"image_path": "",
"sr_ratio": float(sr_ratio),
"target_shape": target_shape,
"return_result_tensor": False,
}
)
if hasattr(runner, "set_progress_callback"):
runner.set_progress_callback(lambda current, _total: progress.update_absolute(current))
runner.run_pipeline(input_info)
run_request(runner, request, runtime_config=runtime_config)
finally:
torch.cuda.empty_cache()
gc.collect()
+3 -3
View File
@@ -2,6 +2,8 @@
name = "lightx2vwrapper"
description = "ComfyUI-Lightx2vWrapper is an inference wrapper for Lightx2v designed for use with ComfyUI."
version = "1.1.0"
readme = "README.md"
requires-python = ">=3.10"
license = { file = "LICENSE" }
[project.urls]
@@ -19,10 +21,8 @@ line-length = 150
indent-width = 4
[tool.ruff.lint]
extend-select = ["I", "F401"]
ignore = ["F"]
extend-select = ["I"]
[tool.ruff.lint.per-file-ignores]
"**/__init__.py" = ["F401"]
"**/lightx2v_kernel/*" = ["F401"]
"**/{cookbook,docs}/*" = ["E402", "F401", "F811", "F841"]