Compare commits
39
Commits
old-master
...
15b88d1689
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15b88d1689 | ||
|
|
211481558c | ||
|
|
333a281eef | ||
|
|
5240e27038 | ||
|
|
a4831d7b71 | ||
|
|
fa6d8a7d88 | ||
|
|
cb60fe2542 | ||
|
|
b2350e7f08 | ||
|
|
5b10a1a594 | ||
|
|
2d96d5aa5d | ||
|
|
0c62c6eef4 | ||
|
|
83e4b5dd98 | ||
|
|
2e75e2d076 | ||
|
|
c08fe58fe7 | ||
|
|
9e84890877 | ||
|
|
2e98e453a4 | ||
|
|
daf0304243 | ||
|
|
5ce7b0edcb | ||
|
|
8d8407ec9d | ||
|
|
91947c0b8c | ||
|
|
c4b69321bb | ||
|
|
f1da0f7876 | ||
|
|
27c5bcf362 | ||
|
|
d2e7db49c7 | ||
|
|
9f66233b53 | ||
|
|
7257c1aa4d | ||
|
|
ebece55ed7 | ||
|
|
a60fb2a25e | ||
|
|
c178f756da | ||
|
|
fb921ae620 | ||
|
|
4723dc329d | ||
|
|
8fe382e5ec | ||
|
|
8311fd0261 | ||
|
|
396dafeefc | ||
|
|
13a89c5831 | ||
|
|
2f1cc17f5c | ||
|
|
b2d7d3b634 | ||
|
|
adc4451716 | ||
|
|
6dd579dcc7 |
@@ -0,0 +1,20 @@
|
|||||||
|
name: Publish to Comfy registry
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths:
|
||||||
|
- "pyproject.toml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-node:
|
||||||
|
name: Publish Custom Node to registry
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Publish Custom Node
|
||||||
|
uses: Comfy-Org/publish-node-action@main
|
||||||
|
with:
|
||||||
|
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
ComfyUI-Tween
|
||||||
|
|
||||||
|
The LDF-VFI sequence sampling adapter in ldf_backend.py is based on the
|
||||||
|
official LDF-VFI implementation:
|
||||||
|
https://github.com/xypeng9903/LDF-VFI
|
||||||
|
|
||||||
|
LDF-VFI is distributed under the Apache License, Version 2.0. Its official
|
||||||
|
runtime is downloaded on demand and retains its upstream LICENSE file.
|
||||||
|
|
||||||
|
SPEED runtime source is not redistributed by this project. It is downloaded
|
||||||
|
on demand from the official repository at a checksum-pinned commit.
|
||||||
@@ -1,40 +1,93 @@
|
|||||||
# ComfyUI BIM-VFI + EMA-VFI + SGM-VFI + GIMM-VFI
|
# Tween — Video Frame Interpolation for ComfyUI
|
||||||
|
|
||||||
ComfyUI custom nodes for video frame interpolation using [BiM-VFI](https://github.com/KAIST-VICLab/BiM-VFI) (CVPR 2025), [EMA-VFI](https://github.com/MCG-NJU/EMA-VFI) (CVPR 2023), [SGM-VFI](https://github.com/MCG-NJU/SGM-VFI) (CVPR 2024), and [GIMM-VFI](https://github.com/GSeanCDAT/GIMM-VFI) (NeurIPS 2024). Designed for long videos with thousands of frames — processes them without running out of VRAM.
|
[](https://registry.comfy.org/)
|
||||||
|
[](https://www.python.org/)
|
||||||
|
[](https://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
[](#which-model-should-i-use)
|
||||||
|
|
||||||
|
Six video frame interpolation models in one package — **BIM-VFI**, **EMA-VFI**, **SGM-VFI**, **GIMM-VFI**, **SPEED**, and **LDF-VFI**. Pairwise models include chunked/segmented processing; LDF-VFI adds holistic long-sequence diffusion interpolation.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="assets/model-comparison.svg" alt="Model Comparison" width="720"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Install from the [ComfyUI Registry](https://registry.comfy.org/) (recommended) or clone manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ComfyUI/custom_nodes
|
||||||
|
git clone https://github.com/Ethanfel/ComfyUI-Tween.git
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Dependencies are declared in `pyproject.toml` and `requirements.txt` and are installed automatically by ComfyUI Manager or pip. There is intentionally no custom `install.py`, avoiding a second redundant dependency-install pass after Manager processes `requirements.txt`. LDF-VFI requires PyTorch 2.5+ plus a current `diffusers`/`accelerate` stack.
|
||||||
|
|
||||||
|
### Demo workflow
|
||||||
|
|
||||||
|
Import [`example_workflows/tween_speed_bim_model_lab.json`](example_workflows/tween_speed_bim_model_lab.json) for the recommended starter graph. It requires [ComfyUI-VideoHelperSuite](https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite) for video loading and encoding.
|
||||||
|
|
||||||
|
- The SPEED and BIM-VFI branches load the same 25-frame, 24 FPS sample, tune memory settings independently, interpolate to 48 FPS, preserve audio, and save separate comparison videos.
|
||||||
|
- Keep the loader's `force_rate`, Tween's `source_fps`/`target_fps`, and Video Combine's `frame_rate` synchronized when changing cadence.
|
||||||
|
|
||||||
|
### cupy (accelerates BIM-VFI, SGM-VFI, and GIMM-VFI)
|
||||||
|
|
||||||
|
[cupy](https://cupy.dev/) provides GPU-accelerated optical flow warping. It is deliberately **not installed automatically**, because replacing or mixing CUDA-specific cupy wheels can disrupt other ComfyUI nodes. BIM-VFI, SGM-VFI, and GIMM-VFI work without it through their PyTorch fallback. **EMA-VFI, SPEED, and LDF-VFI do not use it.**
|
||||||
|
|
||||||
|
1. Find your CUDA version:
|
||||||
|
```bash
|
||||||
|
python -c "import torch; print(torch.version.cuda)"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install the matching package:
|
||||||
|
|
||||||
|
| CUDA | Command |
|
||||||
|
|------|---------|
|
||||||
|
| 13.x | `pip install cupy-cuda13x` |
|
||||||
|
| 12.x | `pip install cupy-cuda12x` |
|
||||||
|
| 11.x | `pip install cupy-cuda11x` |
|
||||||
|
|
||||||
|
> Make sure to run pip in the same Python environment as ComfyUI, and uninstall any different cupy wheel variant first. If cupy is absent or incompatible, Tween safely uses its PyTorch fallback.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>cupy troubleshooting</summary>
|
||||||
|
|
||||||
|
| Problem | Solution |
|
||||||
|
|---------|----------|
|
||||||
|
| `ModuleNotFoundError: No module named 'cupy'` | Install cupy using the steps above |
|
||||||
|
| `cupy` installed but `ImportError` at runtime | CUDA version mismatch — uninstall and reinstall the correct version |
|
||||||
|
| Install hangs or takes very long | Confirm pip selected a prebuilt wheel for your Python and CUDA versions |
|
||||||
|
| Docker / no build tools | Use the matching prebuilt `cupy-cudaXXx` wheel, not bare `cupy` which compiles from source |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## Which model should I use?
|
## Which model should I use?
|
||||||
|
|
||||||
| | BIM-VFI | EMA-VFI | SGM-VFI | GIMM-VFI |
|
| Model | Best for | Multiplier path | Typical VRAM | Trade-off |
|
||||||
|---|---------|---------|---------|----------|
|
|-------|----------|-----------------|--------------|-----------|
|
||||||
| **Best for** | General-purpose, non-uniform motion | Fast inference, light VRAM | Large motion, occlusion-heavy scenes | High multipliers (4x/8x) in a single pass |
|
| **BIM-VFI** | Strong general pairwise quality | Recursive 2x/4x/8x | ~2 GB/pair | Research/education license |
|
||||||
| **Quality** | Highest overall | Good | Best on large motion | Good |
|
| **EMA-VFI** | Speed and lower VRAM | Recursive 2x/4x/8x | ~1.5 GB/pair | Less robust on extreme motion |
|
||||||
| **Speed** | Moderate | Fastest | Slowest | Fast for 4x/8x (single pass) |
|
| **SGM-VFI** | Large motion | Recursive 2x/4x/8x | ~3 GB/pair | Slowest pairwise option |
|
||||||
| **VRAM** | ~2 GB/pair | ~1.5 GB/pair | ~3 GB/pair | ~2.5 GB/pair |
|
| **GIMM-VFI** | Arbitrary timesteps, efficient 4x/8x | Native multi-frame per pair | ~2.5 GB/pair | Still frame-pair-centric |
|
||||||
| **Params** | ~17M | ~14–65M | ~15M + GMFlow | ~80M (RAFT) / ~123M (FlowFormer) |
|
| **SPEED** | New high-quality midpoint generation | One diffusion step at 2x; recursive 4x/8x | ~2.3–2.6 GB at benchmark resolutions | Stochastic, ~447 MB checkpoint |
|
||||||
| **Arbitrary timestep** | Yes | Yes (with `_t` checkpoint) | No (fixed 0.5) | Yes (native single-pass) |
|
| **LDF-VFI** | Long-range temporal coherence and 2x–16x | Native sequence diffusion | ~20 GB | ~6.4 GB weights; much slower |
|
||||||
| **4x/8x mode** | Recursive 2x passes | Recursive 2x passes | Recursive 2x passes | Single forward pass (or recursive) |
|
|
||||||
| **Paper** | CVPR 2025 | CVPR 2023 | CVPR 2024 | NeurIPS 2024 |
|
|
||||||
| **License** | Research only | Apache 2.0 | Apache 2.0 | Apache 2.0 |
|
|
||||||
|
|
||||||
**TL;DR:** Start with **BIM-VFI** for best quality. Use **EMA-VFI** if you need speed or lower VRAM. Use **SGM-VFI** if your video has large camera motion or fast-moving objects that the others struggle with. Use **GIMM-VFI** when you want 4x or 8x interpolation without recursive passes — it generates all intermediate frames in a single forward pass per pair.
|
**TL;DR:** Try **SPEED** as the modern pairwise default. Use **EMA-VFI** when latency matters, **SGM-VFI** for difficult large motion, **GIMM-VFI** for lightweight arbitrary timesteps, and **LDF-VFI** when sequence consistency matters more than speed or memory.
|
||||||
|
|
||||||
|
## VRAM Guide
|
||||||
|
|
||||||
|
| VRAM | Recommended settings |
|
||||||
|
|------|----------------------|
|
||||||
|
| 8 GB | `batch_size=1, chunk_size=500` |
|
||||||
|
| 24 GB | `batch_size=2–4, chunk_size=1000` |
|
||||||
|
| 48 GB+ | `batch_size=4–16, all_on_gpu=true` |
|
||||||
|
| 96 GB+ | `batch_size=8–16, all_on_gpu=true, chunk_size=0` |
|
||||||
|
|
||||||
|
SPEED generally fits the 24 GB tier at HD resolutions. LDF-VFI is a separate workload: its official 8x quick start requires about 20 GB, and higher resolutions may require smaller VAE tiles or more VRAM.
|
||||||
|
|
||||||
## Nodes
|
## Nodes
|
||||||
|
|
||||||
### BIM-VFI
|
The pairwise Interpolate nodes (BIM/EMA/SGM/GIMM/SPEED) share these controls:
|
||||||
|
|
||||||
#### Load BIM-VFI Model
|
|
||||||
|
|
||||||
Loads the BiM-VFI checkpoint. Auto-downloads from Google Drive on first use to `ComfyUI/models/bim-vfi/`.
|
|
||||||
|
|
||||||
| Input | Description |
|
|
||||||
|-------|-------------|
|
|
||||||
| **model_path** | Checkpoint file from `models/bim-vfi/` |
|
|
||||||
| **auto_pyr_level** | Auto-select pyramid level by resolution (<540p=3, 540p=5, 1080p=6, 4K=7) |
|
|
||||||
| **pyr_level** | Manual pyramid level (3-7), only used when auto is off |
|
|
||||||
|
|
||||||
#### BIM-VFI Interpolate
|
|
||||||
|
|
||||||
Interpolates frames from an image batch.
|
|
||||||
|
|
||||||
| Input | Description |
|
| Input | Description |
|
||||||
|-------|-------------|
|
|-------|-------------|
|
||||||
@@ -42,31 +95,56 @@ Interpolates frames from an image batch.
|
|||||||
| **model** | Model from the loader node |
|
| **model** | Model from the loader node |
|
||||||
| **multiplier** | 2x, 4x, or 8x frame rate (recursive 2x passes) |
|
| **multiplier** | 2x, 4x, or 8x frame rate (recursive 2x passes) |
|
||||||
| **batch_size** | Frame pairs processed simultaneously (higher = faster, more VRAM) |
|
| **batch_size** | Frame pairs processed simultaneously (higher = faster, more VRAM) |
|
||||||
| **chunk_size** | Process in segments of N input frames (0 = disabled). Bounds VRAM for very long videos. Result is identical to processing all at once |
|
| **chunk_size** | Process in segments of N input frames (0 = disabled). Bounds VRAM for very long videos |
|
||||||
| **keep_device** | Keep model on GPU between pairs (faster, ~200 MB constant VRAM) |
|
| **keep_device** | Keep model on GPU between pairs (faster, ~200 MB constant VRAM) |
|
||||||
| **all_on_gpu** | Keep all intermediate frames on GPU (fast, needs large VRAM) |
|
| **all_on_gpu** | Keep all intermediate frames on GPU (fast, needs large VRAM) |
|
||||||
| **clear_cache_after_n_frames** | Clear CUDA cache every N pairs to prevent VRAM buildup |
|
| **clear_cache_after_n_frames** | Clear CUDA cache every N pairs to prevent VRAM buildup |
|
||||||
|
| **source_fps** | Input frame rate. Required when target_fps > 0 |
|
||||||
|
| **target_fps** | Target output FPS. When > 0, overrides multiplier — auto-computes a power-of-2 oversample up to 8x, then selects the nearest generated frame for each target timestamp. 0 = use multiplier |
|
||||||
|
|
||||||
#### BIM-VFI Segment Interpolate
|
| Output | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| **images** | Interpolated frames at the target FPS (or at the multiplied rate when target_fps = 0) |
|
||||||
|
| **oversampled** | Full power-of-2 oversampled frames before target FPS selection. Same as `images` when target_fps = 0 |
|
||||||
|
|
||||||
Same as Interpolate but processes a single segment of the input. Chain multiple instances with Save nodes between them to bound peak RAM. The model pass-through output forces sequential execution.
|
<details>
|
||||||
|
<summary><strong>BIM-VFI</strong></summary>
|
||||||
|
|
||||||
### Tween Concat Videos
|
#### Load BIM-VFI Model
|
||||||
|
|
||||||
Concatenates segment video files into a single video using ffmpeg. Connect from any Segment Interpolate's model output to ensure it runs after all segments are saved. Works with all three models.
|
Loads the BiM-VFI checkpoint. Auto-downloads from Google Drive on first use to `ComfyUI/models/bim-vfi/`.
|
||||||
|
|
||||||
### EMA-VFI
|
|
||||||
|
|
||||||
#### Load EMA-VFI Model
|
|
||||||
|
|
||||||
Loads an EMA-VFI checkpoint. Auto-downloads from Google Drive on first use to `ComfyUI/models/ema-vfi/`. Variant (large/small) and timestep support are auto-detected from the filename.
|
|
||||||
|
|
||||||
| Input | Description |
|
| Input | Description |
|
||||||
|-------|-------------|
|
|-------|-------------|
|
||||||
| **model_path** | Checkpoint file from `models/ema-vfi/` |
|
| **model_path** | Checkpoint from `models/bim-vfi/` |
|
||||||
| **tta** | Test-time augmentation: flip input and average with unflipped result (~2x slower, slightly better quality) |
|
| **auto_pyr_level** | Official automatic pyramid policy (below 1080p=5, 1080p=6, 4K=7) |
|
||||||
|
| **pyr_level** | Manual pyramid level (3–7), used when auto is off |
|
||||||
|
| **artifact_safe_mode** | Disables the RGB refinement residual to suppress wrong-edge/halo artifacts caused by flow misalignment in blurry or large-motion shots. Off preserves official behavior and can retain more detail on easy shots |
|
||||||
|
|
||||||
|
`artifact_safe_mode` implements the workaround recommended by the official BIM-VFI maintainer for [wrong-edge artifacts caused by severely misaligned warped inputs](https://github.com/KAIST-VICLab/BiM-VFI/issues/1). Enable it selectively for affected footage.
|
||||||
|
|
||||||
|
#### BIM-VFI Interpolate
|
||||||
|
|
||||||
|
Common controls listed above.
|
||||||
|
|
||||||
|
#### BIM-VFI Segment Interpolate
|
||||||
|
|
||||||
|
Processes a single segment of the input. Chain multiple instances with Save nodes between them to bound peak RAM. The model pass-through output forces sequential execution.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>EMA-VFI</strong></summary>
|
||||||
|
|
||||||
|
#### Load EMA-VFI Model
|
||||||
|
|
||||||
|
Auto-downloads from Google Drive to `ComfyUI/models/ema-vfi/`. Variant and timestep support are auto-detected from the filename.
|
||||||
|
|
||||||
|
| Input | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| **model_path** | Checkpoint from `models/ema-vfi/` |
|
||||||
|
| **tta** | Test-time augmentation (~2x slower, slightly better quality) |
|
||||||
|
|
||||||
Available checkpoints:
|
|
||||||
| Checkpoint | Variant | Params | Arbitrary timestep |
|
| Checkpoint | Variant | Params | Arbitrary timestep |
|
||||||
|-----------|---------|--------|-------------------|
|
|-----------|---------|--------|-------------------|
|
||||||
| `ours_t.pkl` | Large | ~65 M | Yes |
|
| `ours_t.pkl` | Large | ~65 M | Yes |
|
||||||
@@ -74,51 +152,47 @@ Available checkpoints:
|
|||||||
| `ours_small_t.pkl` | Small | ~14 M | Yes |
|
| `ours_small_t.pkl` | Small | ~14 M | Yes |
|
||||||
| `ours_small.pkl` | Small | ~14 M | No (fixed 0.5) |
|
| `ours_small.pkl` | Small | ~14 M | No (fixed 0.5) |
|
||||||
|
|
||||||
#### EMA-VFI Interpolate
|
#### EMA-VFI Interpolate / Segment Interpolate
|
||||||
|
|
||||||
Interpolates frames from an image batch. Same controls as BIM-VFI Interpolate.
|
Same controls as above.
|
||||||
|
|
||||||
#### EMA-VFI Segment Interpolate
|
</details>
|
||||||
|
|
||||||
Same as EMA-VFI Interpolate but processes a single segment. Same pattern as BIM-VFI Segment Interpolate.
|
<details>
|
||||||
|
<summary><strong>SGM-VFI</strong></summary>
|
||||||
### SGM-VFI
|
|
||||||
|
|
||||||
#### Load SGM-VFI Model
|
#### Load SGM-VFI Model
|
||||||
|
|
||||||
Loads an SGM-VFI checkpoint. Auto-downloads from Google Drive on first use to `ComfyUI/models/sgm-vfi/`. Variant (base/small) is auto-detected from the filename (default is small).
|
Auto-downloads from Google Drive to `ComfyUI/models/sgm-vfi/`. Requires cupy.
|
||||||
|
|
||||||
| Input | Description |
|
| Input | Description |
|
||||||
|-------|-------------|
|
|-------|-------------|
|
||||||
| **model_path** | Checkpoint file from `models/sgm-vfi/` |
|
| **model_path** | Checkpoint from `models/sgm-vfi/` |
|
||||||
| **tta** | Test-time augmentation: flip input and average with unflipped result (~2x slower, slightly better quality) |
|
| **tta** | Test-time augmentation (~2x slower, slightly better quality) |
|
||||||
| **num_key_points** | Sparsity of global matching (0.0 = global everywhere, 0.5 = default balance, higher = faster) |
|
| **num_key_points** | Global matching sparsity (0.0 = global everywhere, 0.5 = default, higher = faster) |
|
||||||
|
|
||||||
Available checkpoints:
|
|
||||||
| Checkpoint | Variant | Params |
|
| Checkpoint | Variant | Params |
|
||||||
|-----------|---------|--------|
|
|-----------|---------|--------|
|
||||||
| `ours-1-2-points.pkl` | Small | ~15 M + GMFlow |
|
| `ours-1-2-points.pkl` | Small | ~15 M + GMFlow |
|
||||||
|
|
||||||
#### SGM-VFI Interpolate
|
#### SGM-VFI Interpolate / Segment Interpolate
|
||||||
|
|
||||||
Interpolates frames from an image batch. Same controls as BIM-VFI Interpolate.
|
Same controls as above.
|
||||||
|
|
||||||
#### SGM-VFI Segment Interpolate
|
</details>
|
||||||
|
|
||||||
Same as SGM-VFI Interpolate but processes a single segment. Same pattern as BIM-VFI Segment Interpolate.
|
<details>
|
||||||
|
<summary><strong>GIMM-VFI</strong></summary>
|
||||||
### GIMM-VFI
|
|
||||||
|
|
||||||
#### Load GIMM-VFI Model
|
#### Load GIMM-VFI Model
|
||||||
|
|
||||||
Loads a GIMM-VFI checkpoint. Auto-downloads from [HuggingFace](https://huggingface.co/Kijai/GIMM-VFI_safetensors) on first use to `ComfyUI/models/gimm-vfi/`. The matching flow estimator (RAFT or FlowFormer) is auto-detected and downloaded alongside the main model.
|
Auto-downloads from [HuggingFace](https://huggingface.co/Kijai/GIMM-VFI_safetensors) to `ComfyUI/models/gimm-vfi/`. The matching flow estimator (RAFT or FlowFormer) is auto-detected and downloaded alongside.
|
||||||
|
|
||||||
| Input | Description |
|
| Input | Description |
|
||||||
|-------|-------------|
|
|-------|-------------|
|
||||||
| **model_path** | Checkpoint file from `models/gimm-vfi/` |
|
| **model_path** | Checkpoint from `models/gimm-vfi/` |
|
||||||
| **ds_factor** | Downscale factor for internal processing (1.0 = full res, 0.5 = half). Lower = less VRAM, faster, less quality. Try 0.5 for 4K inputs |
|
| **ds_factor** | Downscale factor for internal processing (1.0 = full, 0.5 = half). Try 0.5 for 4K inputs |
|
||||||
|
|
||||||
Available checkpoints:
|
|
||||||
| Checkpoint | Variant | Params | Flow estimator (auto-downloaded) |
|
| Checkpoint | Variant | Params | Flow estimator (auto-downloaded) |
|
||||||
|-----------|---------|--------|----------------------------------|
|
|-----------|---------|--------|----------------------------------|
|
||||||
| `gimmvfi_r_arb_lpips_fp32.safetensors` | RAFT | ~80 M | `raft-things_fp32.safetensors` |
|
| `gimmvfi_r_arb_lpips_fp32.safetensors` | RAFT | ~80 M | `raft-things_fp32.safetensors` |
|
||||||
@@ -126,68 +200,93 @@ Available checkpoints:
|
|||||||
|
|
||||||
#### GIMM-VFI Interpolate
|
#### GIMM-VFI Interpolate
|
||||||
|
|
||||||
Interpolates frames from an image batch. Same controls as BIM-VFI Interpolate, plus:
|
Common controls plus:
|
||||||
|
|
||||||
| Input | Description |
|
| Input | Description |
|
||||||
|-------|-------------|
|
|-------|-------------|
|
||||||
| **single_pass** | When enabled (default), generates all intermediate frames per pair in one forward pass using GIMM-VFI's arbitrary-timestep capability. No recursive 2x passes needed for 4x or 8x. Disable to use the standard recursive approach (same as BIM/EMA/SGM) |
|
| **single_pass** | Generate all intermediate frames per pair in one forward pass (default on). No recursive 2x passes needed for 4x/8x. Disable to use the standard recursive approach |
|
||||||
|
|
||||||
#### GIMM-VFI Segment Interpolate
|
#### GIMM-VFI Segment Interpolate
|
||||||
|
|
||||||
Same as GIMM-VFI Interpolate but processes a single segment. Same pattern as BIM-VFI Segment Interpolate.
|
Same pattern as other Segment nodes.
|
||||||
|
|
||||||
**Output frame count (all models):** 2x = 2N-1, 4x = 4N-3, 8x = 8N-7
|
</details>
|
||||||
|
|
||||||
## Installation
|
<details>
|
||||||
|
<summary><strong>SPEED</strong></summary>
|
||||||
|
|
||||||
Clone into your ComfyUI `custom_nodes/` directory:
|
#### Load SPEED Model
|
||||||
|
|
||||||
```bash
|
Downloads the official `speed.pt` checkpoint from [zhZ524/SPEED](https://huggingface.co/zhZ524/SPEED) to `ComfyUI/models/speed-vfi/`. The loader also fetches a checksum-pinned snapshot of the official runtime on first use; Tween does not bundle that source.
|
||||||
cd ComfyUI/custom_nodes
|
|
||||||
git clone https://github.com/your-user/ComfyUI-Tween.git
|
|
||||||
```
|
|
||||||
|
|
||||||
Dependencies (`gdown`, `cupy`, `timm`, `omegaconf`, `easydict`, `yacs`, `einops`, `huggingface_hub`) are auto-installed on first load. The correct `cupy` variant is detected from your PyTorch CUDA version.
|
| Input | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| **model_path** | Checkpoint from `models/speed-vfi/` (official default is ~447 MB) |
|
||||||
|
| **precision** | `auto` prefers BF16, then FP16; FP32 is available for comparison |
|
||||||
|
|
||||||
> **Warning:** `cupy` is a large package (~800MB) and compilation/installation can take several minutes. The first ComfyUI startup after installing this node may appear to hang while `cupy` installs in the background. Check the console log for progress. If auto-install fails (e.g. missing build tools in Docker), install manually with:
|
#### SPEED Interpolate / Segment Interpolate
|
||||||
> ```bash
|
|
||||||
> pip install cupy-cuda12x # replace 12 with your CUDA major version
|
|
||||||
> ```
|
|
||||||
|
|
||||||
To install manually:
|
Uses the same batching, chunking, segment, and exact-target-FPS controls as BIM-VFI, plus a `seed` input for repeatable starting pixel noise. Keeping the seed on the interpolation node lets it change without reloading the model. SPEED is repeatable for the same seed and execution settings; changing batch, chunk, or segment boundaries can change how its stochastic noise is assigned. The released model predicts only the midpoint, so 4x and 8x are recursive passes. Inputs are padded to the model's 64-pixel divisor and cropped back automatically.
|
||||||
|
|
||||||
```bash
|
</details>
|
||||||
cd ComfyUI-Tween
|
|
||||||
python install.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### Requirements
|
<details>
|
||||||
|
<summary><strong>LDF-VFI</strong></summary>
|
||||||
|
|
||||||
- PyTorch with CUDA
|
#### Load LDF-VFI Model
|
||||||
- `cupy` (matching your CUDA version, for BIM-VFI, SGM-VFI, and GIMM-VFI)
|
|
||||||
- `timm` (for EMA-VFI and SGM-VFI)
|
|
||||||
- `gdown` (for BIM-VFI/EMA-VFI/SGM-VFI model auto-download)
|
|
||||||
- `omegaconf`, `easydict`, `yacs`, `einops` (for GIMM-VFI)
|
|
||||||
- `huggingface_hub` (for GIMM-VFI model auto-download)
|
|
||||||
|
|
||||||
## VRAM Guide
|
Downloads the official transformer and conditional VAE from [onecat-ai/LDF-VFI](https://huggingface.co/onecat-ai/LDF-VFI) to `ComfyUI/models/ldf-vfi/` (~6.4 GB total). A checksum-pinned Apache-2.0 runtime snapshot is fetched on first use. Loading stays on CPU until the interpolation node executes.
|
||||||
|
|
||||||
| VRAM | Recommended settings |
|
| Input | Description |
|
||||||
|------|---------------------|
|
|-------|-------------|
|
||||||
| 8 GB | batch_size=1, chunk_size=500 |
|
| **tile_size / tile_overlap** | Spatial VAE tiling and seam blending; default 256/64 |
|
||||||
| 24 GB | batch_size=2-4, chunk_size=1000 |
|
| **vae_batch_size** | Lower first if VAE encode/decode runs out of VRAM |
|
||||||
| 48 GB+ | batch_size=4-16, all_on_gpu=true |
|
| **attention_type** | Official `slide_chunk_all_block_2x1x1` sparse attention is recommended |
|
||||||
| 96 GB+ | batch_size=8-16, all_on_gpu=true, chunk_size=0 |
|
|
||||||
|
#### LDF-VFI Sequence Interpolate
|
||||||
|
|
||||||
|
LDF-VFI is not a pairwise node. It processes the ordered source batch with the paper's skip-concat autoregressive sampler and internally chunks long sequences without breaking temporal context.
|
||||||
|
|
||||||
|
| Input | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| **temporal_factor** | Any integer from 2x through 16x |
|
||||||
|
| **sampling_steps** | Diffusion steps per temporal block; official quick start uses 16 |
|
||||||
|
| **t_shift / t_cond** | Official defaults are 8.0 / 0.1 |
|
||||||
|
| **seed** | Repeatable VAE and diffusion sampling |
|
||||||
|
| **offload_after** | Return transformer and VAE to CPU after generation |
|
||||||
|
| **source_fps / target_fps** | Optional exact-FPS selection using the smallest sufficient native factor |
|
||||||
|
|
||||||
|
The second output, `generated_sequence`, is the full native-factor sequence before exact-FPS selection. LDF has no Segment node because externally splitting the sequence would discard the long-range context it is designed to preserve.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Tween Concat Videos
|
||||||
|
|
||||||
|
Concatenates segment video files into a single video using ffmpeg. Connect from any pairwise Segment Interpolate's model output to ensure it runs after all segments are saved.
|
||||||
|
|
||||||
|
### Output frame count
|
||||||
|
|
||||||
|
- **Pairwise multiplier mode:** 2x = 2N-1, 4x = 4N-3, 8x = 8N-7
|
||||||
|
- **LDF-VFI native factor:** factor `F` = `F(N-1)+1`, for any integer `F` from 2 through 16
|
||||||
|
- **Target FPS mode:** `floor((N-1) / source_fps * target_fps) + 1` frames. Pairwise nodes oversample to the nearest power-of-2 above the ratio (up to 8x), then select the nearest generated frame for each target timestamp. Downsampling (target < source) also works — frames are selected from the input with no model calls. LDF-VFI supports native factors up to 16x.
|
||||||
|
|
||||||
|
In target-FPS Segment mode, a very small `segment_size` can cover less than one output-frame interval while downsampling. Increase `segment_size` if the node reports that the segment contains no target timestamps; returning a placeholder frame would make concatenated timing incorrect.
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
This project wraps the official [BiM-VFI](https://github.com/KAIST-VICLab/BiM-VFI) implementation by the [KAIST VIC Lab](https://github.com/KAIST-VICLab), the official [EMA-VFI](https://github.com/MCG-NJU/EMA-VFI) implementation by MCG-NJU, the official [SGM-VFI](https://github.com/MCG-NJU/SGM-VFI) implementation by MCG-NJU, and the [GIMM-VFI](https://github.com/GSeanCDAT/GIMM-VFI) implementation by S-Lab (NTU). GIMM-VFI architecture files in `gimm_vfi_arch/` are adapted from [kijai/ComfyUI-GIMM-VFI](https://github.com/kijai/ComfyUI-GIMM-VFI) with safetensors checkpoints from [Kijai/GIMM-VFI_safetensors](https://huggingface.co/Kijai/GIMM-VFI_safetensors). Architecture files in `bim_vfi_arch/`, `ema_vfi_arch/`, `sgm_vfi_arch/`, and `gimm_vfi_arch/` are vendored from their respective repositories with minimal modifications (relative imports, device-awareness fixes, inference-only paths).
|
| Model | Authors | Venue | Links |
|
||||||
|
|-------|---------|-------|-------|
|
||||||
|
| **BIM-VFI** | Seo, Oh, Kim (KAIST VIC Lab) | CVPR 2025 | [Paper](https://arxiv.org/abs/2412.11365) · [Code](https://github.com/KAIST-VICLab/BiM-VFI) · [Project](https://kaist-viclab.github.io/BiM-VFI_site/) |
|
||||||
|
| **EMA-VFI** | Zhang et al. (MCG-NJU) | CVPR 2023 | [Paper](https://arxiv.org/abs/2303.00440) · [Code](https://github.com/MCG-NJU/EMA-VFI) |
|
||||||
|
| **SGM-VFI** | Zhang et al. (MCG-NJU) | CVPR 2024 | [Paper](https://arxiv.org/abs/2404.06913) · [Code](https://github.com/MCG-NJU/SGM-VFI) |
|
||||||
|
| **GIMM-VFI** | Guo, Li, Loy (S-Lab NTU) | NeurIPS 2024 | [Paper](https://arxiv.org/abs/2407.08680) · [Code](https://github.com/GSeanCDAT/GIMM-VFI) |
|
||||||
|
| **SPEED** | Zhang et al. | ACM MM 2026 | [Paper](https://arxiv.org/abs/2607.15585) · [Code](https://github.com/bbldCVer/SPEED) · [Model](https://huggingface.co/zhZ524/SPEED) |
|
||||||
|
| **LDF-VFI** | Peng et al. | CVPR 2026 | [Paper](https://arxiv.org/abs/2601.14959) · [Code](https://github.com/xypeng9903/LDF-VFI) · [Model](https://huggingface.co/onecat-ai/LDF-VFI) |
|
||||||
|
|
||||||
**BiM-VFI:**
|
GIMM-VFI adaptation from [kijai/ComfyUI-GIMM-VFI](https://github.com/kijai/ComfyUI-GIMM-VFI) with checkpoints from [Kijai/GIMM-VFI_safetensors](https://huggingface.co/Kijai/GIMM-VFI_safetensors). Architecture files in `bim_vfi_arch/`, `ema_vfi_arch/`, `sgm_vfi_arch/`, and `gimm_vfi_arch/` are vendored from their respective repositories with minimal modifications. SPEED and LDF-VFI use checksum-pinned official source snapshots downloaded into their model directories on demand.
|
||||||
> Wonyong Seo, Jihyong Oh, and Munchurl Kim.
|
|
||||||
> "BiM-VFI: Bidirectional Motion Field-Guided Frame Interpolation for Video with Non-uniform Motions."
|
<details>
|
||||||
> *IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)*, 2025.
|
<summary>BibTeX citations</summary>
|
||||||
> [[arXiv]](https://arxiv.org/abs/2412.11365) [[Project Page]](https://kaist-viclab.github.io/BiM-VFI_site/) [[GitHub]](https://github.com/KAIST-VICLab/BiM-VFI)
|
|
||||||
|
|
||||||
```bibtex
|
```bibtex
|
||||||
@inproceedings{seo2025bimvfi,
|
@inproceedings{seo2025bimvfi,
|
||||||
@@ -196,59 +295,53 @@ This project wraps the official [BiM-VFI](https://github.com/KAIST-VICLab/BiM-VF
|
|||||||
booktitle={Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)},
|
booktitle={Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)},
|
||||||
year={2025}
|
year={2025}
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
**EMA-VFI:**
|
|
||||||
> Guozhen Zhang, Yuhan Zhu, Haonan Wang, Youxin Chen, Gangshan Wu, and Limin Wang.
|
|
||||||
> "Extracting Motion and Appearance via Inter-Frame Attention for Efficient Video Frame Interpolation."
|
|
||||||
> *IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)*, 2023.
|
|
||||||
> [[arXiv]](https://arxiv.org/abs/2303.00440) [[GitHub]](https://github.com/MCG-NJU/EMA-VFI)
|
|
||||||
|
|
||||||
```bibtex
|
|
||||||
@inproceedings{zhang2023emavfi,
|
@inproceedings{zhang2023emavfi,
|
||||||
title={Extracting Motion and Appearance via Inter-Frame Attention for Efficient Video Frame Interpolation},
|
title={Extracting Motion and Appearance via Inter-Frame Attention for Efficient Video Frame Interpolation},
|
||||||
author={Zhang, Guozhen and Zhu, Yuhan and Wang, Haonan and Chen, Youxin and Wu, Gangshan and Wang, Limin},
|
author={Zhang, Guozhen and Zhu, Yuhan and Wang, Haonan and Chen, Youxin and Wu, Gangshan and Wang, Limin},
|
||||||
booktitle={Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)},
|
booktitle={Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)},
|
||||||
year={2023}
|
year={2023}
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
**SGM-VFI:**
|
|
||||||
> Guozhen Zhang, Yuhan Zhu, Evan Zheran Liu, Haonan Wang, Mingzhen Sun, Gangshan Wu, and Limin Wang.
|
|
||||||
> "Sparse Global Matching for Video Frame Interpolation with Large Motion."
|
|
||||||
> *IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)*, 2024.
|
|
||||||
> [[arXiv]](https://arxiv.org/abs/2404.06913) [[GitHub]](https://github.com/MCG-NJU/SGM-VFI)
|
|
||||||
|
|
||||||
```bibtex
|
|
||||||
@inproceedings{zhang2024sgmvfi,
|
@inproceedings{zhang2024sgmvfi,
|
||||||
title={Sparse Global Matching for Video Frame Interpolation with Large Motion},
|
title={Sparse Global Matching for Video Frame Interpolation with Large Motion},
|
||||||
author={Zhang, Guozhen and Zhu, Yuhan and Liu, Evan Zheran and Wang, Haonan and Sun, Mingzhen and Wu, Gangshan and Wang, Limin},
|
author={Zhang, Guozhen and Zhu, Yuhan and Liu, Evan Zheran and Wang, Haonan and Sun, Mingzhen and Wu, Gangshan and Wang, Limin},
|
||||||
booktitle={Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)},
|
booktitle={Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)},
|
||||||
year={2024}
|
year={2024}
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
**GIMM-VFI:**
|
|
||||||
> Zujin Guo, Wei Li, and Chen Change Loy.
|
|
||||||
> "Generalizable Implicit Motion Modeling for Video Frame Interpolation."
|
|
||||||
> *Advances in Neural Information Processing Systems (NeurIPS)*, 2024.
|
|
||||||
> [[arXiv]](https://arxiv.org/abs/2407.08680) [[GitHub]](https://github.com/GSeanCDAT/GIMM-VFI)
|
|
||||||
|
|
||||||
```bibtex
|
|
||||||
@inproceedings{guo2024gimmvfi,
|
@inproceedings{guo2024gimmvfi,
|
||||||
title={Generalizable Implicit Motion Modeling for Video Frame Interpolation},
|
title={Generalizable Implicit Motion Modeling for Video Frame Interpolation},
|
||||||
author={Guo, Zujin and Li, Wei and Loy, Chen Change},
|
author={Guo, Zujin and Li, Wei and Loy, Chen Change},
|
||||||
booktitle={Advances in Neural Information Processing Systems (NeurIPS)},
|
booktitle={Advances in Neural Information Processing Systems (NeurIPS)},
|
||||||
year={2024}
|
year={2024}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@misc{zhang2026speed,
|
||||||
|
title={SPEED: One-Step Pixel Diffusion for High-quality Video Frame Interpolation},
|
||||||
|
author={Zhang, Zihao and Zhao, Haoyu and Yang, Siqian and Wu, Yidi and Jiang, Yudong and Wu, Zuxuan},
|
||||||
|
year={2026},
|
||||||
|
eprint={2607.15585},
|
||||||
|
archivePrefix={arXiv}
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{peng2026holistic,
|
||||||
|
title={Towards Holistic Modeling for Video Frame Interpolation with Auto-regressive Diffusion Transformers},
|
||||||
|
author={Peng, Xinyu and Li, Han and Huang, Yuyang and Zheng, Ziyang and Wang, Yaoming and Chen, Xin and Dai, Wenrui and Li, Chenglin and Zou, Junni and Xiong, Hongkai},
|
||||||
|
year={2026},
|
||||||
|
eprint={2601.14959},
|
||||||
|
archivePrefix={arXiv}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
The BiM-VFI model weights and architecture code are provided by KAIST VIC Lab for **research and education purposes only**. Commercial use requires permission from the principal investigator (Prof. Munchurl Kim, mkimee@kaist.ac.kr). See the [original repository](https://github.com/KAIST-VICLab/BiM-VFI) for details.
|
**BIM-VFI:** Research and education only. Commercial use requires permission from Prof. Munchurl Kim (mkimee@kaist.ac.kr). See the [original repository](https://github.com/KAIST-VICLab/BiM-VFI).
|
||||||
|
|
||||||
The EMA-VFI model weights and architecture code are released under the [Apache 2.0 License](https://github.com/MCG-NJU/EMA-VFI/blob/main/LICENSE). See the [original repository](https://github.com/MCG-NJU/EMA-VFI) for details.
|
**EMA-VFI, SGM-VFI, GIMM-VFI, LDF-VFI:** [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0). GIMM-VFI ComfyUI adaptation based on [kijai/ComfyUI-GIMM-VFI](https://github.com/kijai/ComfyUI-GIMM-VFI).
|
||||||
|
|
||||||
The SGM-VFI model weights and architecture code are released under the [Apache 2.0 License](https://github.com/MCG-NJU/SGM-VFI/blob/main/LICENSE). See the [original repository](https://github.com/MCG-NJU/SGM-VFI) for details.
|
**SPEED:** The official source repository did not include a license file when this integration was pinned. Tween does not redistribute that source; the loader downloads it directly from the official repository. Review the upstream terms before redistribution or commercial use. The checkpoint is likewise downloaded from its official Hugging Face repository.
|
||||||
|
|
||||||
The GIMM-VFI model weights and architecture code are released under the [Apache 2.0 License](https://github.com/GSeanCDAT/GIMM-VFI/blob/main/LICENSE). See the [original repository](https://github.com/GSeanCDAT/GIMM-VFI) for details. ComfyUI adaptation based on [kijai/ComfyUI-GIMM-VFI](https://github.com/kijai/ComfyUI-GIMM-VFI).
|
**This wrapper code:** [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
|||||||
+15
-49
@@ -1,59 +1,13 @@
|
|||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger("Tween")
|
|
||||||
|
|
||||||
|
|
||||||
def _auto_install_deps():
|
|
||||||
"""Auto-install missing dependencies on first load."""
|
|
||||||
# gdown
|
|
||||||
try:
|
|
||||||
import gdown # noqa: F401
|
|
||||||
except ImportError:
|
|
||||||
logger.info("[Tween] Installing gdown...")
|
|
||||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "gdown"])
|
|
||||||
|
|
||||||
# timm (required for EMA-VFI's MotionFormer backbone)
|
|
||||||
try:
|
|
||||||
import timm # noqa: F401
|
|
||||||
except ImportError:
|
|
||||||
logger.info("[Tween] Installing timm...")
|
|
||||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "timm"])
|
|
||||||
|
|
||||||
# cupy
|
|
||||||
try:
|
|
||||||
import cupy # noqa: F401
|
|
||||||
except ImportError:
|
|
||||||
try:
|
|
||||||
import torch
|
|
||||||
major = int(torch.version.cuda.split(".")[0])
|
|
||||||
cupy_pkg = f"cupy-cuda{major}x"
|
|
||||||
logger.info(f"[Tween] Installing {cupy_pkg} (CUDA {torch.version.cuda})...")
|
|
||||||
subprocess.check_call([sys.executable, "-m", "pip", "install", cupy_pkg])
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"[Tween] Could not auto-install cupy: {e}")
|
|
||||||
|
|
||||||
# GIMM-VFI dependencies
|
|
||||||
for pkg in ("omegaconf", "yacs", "easydict", "einops", "huggingface_hub"):
|
|
||||||
try:
|
|
||||||
__import__(pkg)
|
|
||||||
except ImportError:
|
|
||||||
logger.info(f"[Tween] Installing {pkg}...")
|
|
||||||
subprocess.check_call([sys.executable, "-m", "pip", "install", pkg])
|
|
||||||
|
|
||||||
|
|
||||||
_auto_install_deps()
|
|
||||||
|
|
||||||
from .nodes import (
|
from .nodes import (
|
||||||
LoadBIMVFIModel, BIMVFIInterpolate, BIMVFISegmentInterpolate, TweenConcatVideos,
|
LoadBIMVFIModel, BIMVFIInterpolate, BIMVFISegmentInterpolate, TweenConcatVideos,
|
||||||
LoadEMAVFIModel, EMAVFIInterpolate, EMAVFISegmentInterpolate,
|
LoadEMAVFIModel, EMAVFIInterpolate, EMAVFISegmentInterpolate,
|
||||||
LoadSGMVFIModel, SGMVFIInterpolate, SGMVFISegmentInterpolate,
|
LoadSGMVFIModel, SGMVFIInterpolate, SGMVFISegmentInterpolate,
|
||||||
LoadGIMMVFIModel, GIMMVFIInterpolate, GIMMVFISegmentInterpolate,
|
LoadGIMMVFIModel, GIMMVFIInterpolate, GIMMVFISegmentInterpolate,
|
||||||
|
LoadSPEEDVFIModel, SPEEDVFIInterpolate, SPEEDVFISegmentInterpolate,
|
||||||
|
LoadLDFVFIModel, LDFVFIInterpolate,
|
||||||
|
VFIOptimizer,
|
||||||
)
|
)
|
||||||
|
|
||||||
WEB_DIRECTORY = "./web"
|
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
NODE_CLASS_MAPPINGS = {
|
||||||
"LoadBIMVFIModel": LoadBIMVFIModel,
|
"LoadBIMVFIModel": LoadBIMVFIModel,
|
||||||
"BIMVFIInterpolate": BIMVFIInterpolate,
|
"BIMVFIInterpolate": BIMVFIInterpolate,
|
||||||
@@ -68,6 +22,12 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
"LoadGIMMVFIModel": LoadGIMMVFIModel,
|
"LoadGIMMVFIModel": LoadGIMMVFIModel,
|
||||||
"GIMMVFIInterpolate": GIMMVFIInterpolate,
|
"GIMMVFIInterpolate": GIMMVFIInterpolate,
|
||||||
"GIMMVFISegmentInterpolate": GIMMVFISegmentInterpolate,
|
"GIMMVFISegmentInterpolate": GIMMVFISegmentInterpolate,
|
||||||
|
"LoadSPEEDVFIModel": LoadSPEEDVFIModel,
|
||||||
|
"SPEEDVFIInterpolate": SPEEDVFIInterpolate,
|
||||||
|
"SPEEDVFISegmentInterpolate": SPEEDVFISegmentInterpolate,
|
||||||
|
"LoadLDFVFIModel": LoadLDFVFIModel,
|
||||||
|
"LDFVFIInterpolate": LDFVFIInterpolate,
|
||||||
|
"VFIOptimizer": VFIOptimizer,
|
||||||
}
|
}
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
@@ -84,4 +44,10 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"LoadGIMMVFIModel": "Load GIMM-VFI Model",
|
"LoadGIMMVFIModel": "Load GIMM-VFI Model",
|
||||||
"GIMMVFIInterpolate": "GIMM-VFI Interpolate",
|
"GIMMVFIInterpolate": "GIMM-VFI Interpolate",
|
||||||
"GIMMVFISegmentInterpolate": "GIMM-VFI Segment Interpolate",
|
"GIMMVFISegmentInterpolate": "GIMM-VFI Segment Interpolate",
|
||||||
|
"LoadSPEEDVFIModel": "Load SPEED Model",
|
||||||
|
"SPEEDVFIInterpolate": "SPEED Interpolate",
|
||||||
|
"SPEEDVFISegmentInterpolate": "SPEED Segment Interpolate",
|
||||||
|
"LoadLDFVFIModel": "Load LDF-VFI Model",
|
||||||
|
"LDFVFIInterpolate": "LDF-VFI Sequence Interpolate",
|
||||||
|
"VFIOptimizer": "VFI Optimizer",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 475" width="720" height="475">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="gQ" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0%" stop-color="#7aa2f7"/><stop offset="100%" stop-color="#7dcfff"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="gS" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0%" stop-color="#9ece6a"/><stop offset="100%" stop-color="#73daca"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="gV" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0%" stop-color="#bb9af7"/><stop offset="100%" stop-color="#d2a8ff"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background -->
|
||||||
|
<rect width="720" height="475" rx="16" fill="#0d1117"/>
|
||||||
|
|
||||||
|
<!-- ═══ BIM-VFI (top-left) ═══ -->
|
||||||
|
<rect x="10" y="10" width="340" height="145" rx="10" fill="#161b22" stroke="#30363d" stroke-width="1"/>
|
||||||
|
<rect x="11" y="22" width="3" height="121" fill="#3fb950"/>
|
||||||
|
<text x="30" y="38" fill="#e6edf3" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="15" font-weight="600">BIM-VFI</text>
|
||||||
|
<text x="30" y="56" fill="#3fb950" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">Strong pairwise quality · CVPR 2025</text>
|
||||||
|
<line x1="30" y1="64" x2="330" y2="64" stroke="#30363d" stroke-width="0.5"/>
|
||||||
|
<text x="30" y="82" fill="#7aa2f7" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">Quality</text>
|
||||||
|
<rect x="88" y="72" width="244" height="11" rx="3" fill="#21262d"/>
|
||||||
|
<rect x="88" y="72" width="244" height="11" rx="3" fill="url(#gQ)" opacity="0.85"/>
|
||||||
|
<text x="30" y="100" fill="#9ece6a" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">Speed</text>
|
||||||
|
<rect x="88" y="90" width="244" height="11" rx="3" fill="#21262d"/>
|
||||||
|
<rect x="88" y="90" width="146" height="11" rx="3" fill="url(#gS)" opacity="0.85"/>
|
||||||
|
<text x="30" y="118" fill="#bb9af7" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">VRAM</text>
|
||||||
|
<rect x="88" y="108" width="244" height="11" rx="3" fill="#21262d"/>
|
||||||
|
<rect x="88" y="108" width="195" height="11" rx="3" fill="url(#gV)" opacity="0.85"/>
|
||||||
|
<text x="262" y="143" fill="#f0883e" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="10">Research only</text>
|
||||||
|
|
||||||
|
<!-- ═══ EMA-VFI (top-right) ═══ -->
|
||||||
|
<rect x="370" y="10" width="340" height="145" rx="10" fill="#161b22" stroke="#30363d" stroke-width="1"/>
|
||||||
|
<rect x="371" y="22" width="3" height="121" fill="#58a6ff"/>
|
||||||
|
<text x="390" y="38" fill="#e6edf3" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="15" font-weight="600">EMA-VFI</text>
|
||||||
|
<text x="390" y="56" fill="#58a6ff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">Fastest · No cupy needed · CVPR 2023</text>
|
||||||
|
<line x1="390" y1="64" x2="690" y2="64" stroke="#30363d" stroke-width="0.5"/>
|
||||||
|
<text x="390" y="82" fill="#7aa2f7" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">Quality</text>
|
||||||
|
<rect x="448" y="72" width="244" height="11" rx="3" fill="#21262d"/>
|
||||||
|
<rect x="448" y="72" width="146" height="11" rx="3" fill="url(#gQ)" opacity="0.85"/>
|
||||||
|
<text x="390" y="100" fill="#9ece6a" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">Speed</text>
|
||||||
|
<rect x="448" y="90" width="244" height="11" rx="3" fill="#21262d"/>
|
||||||
|
<rect x="448" y="90" width="244" height="11" rx="3" fill="url(#gS)" opacity="0.85"/>
|
||||||
|
<text x="390" y="118" fill="#bb9af7" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">VRAM</text>
|
||||||
|
<rect x="448" y="108" width="244" height="11" rx="3" fill="#21262d"/>
|
||||||
|
<rect x="448" y="108" width="244" height="11" rx="3" fill="url(#gV)" opacity="0.85"/>
|
||||||
|
<text x="632" y="143" fill="#8b949e" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="10">Apache 2.0</text>
|
||||||
|
|
||||||
|
<!-- ═══ SGM-VFI (bottom-left) ═══ -->
|
||||||
|
<rect x="10" y="165" width="340" height="145" rx="10" fill="#161b22" stroke="#30363d" stroke-width="1"/>
|
||||||
|
<rect x="11" y="177" width="3" height="121" fill="#f0883e"/>
|
||||||
|
<text x="30" y="193" fill="#e6edf3" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="15" font-weight="600">SGM-VFI</text>
|
||||||
|
<text x="30" y="211" fill="#f0883e" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">Large motion specialist · CVPR 2024</text>
|
||||||
|
<line x1="30" y1="219" x2="330" y2="219" stroke="#30363d" stroke-width="0.5"/>
|
||||||
|
<text x="30" y="237" fill="#7aa2f7" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">Quality</text>
|
||||||
|
<rect x="88" y="227" width="244" height="11" rx="3" fill="#21262d"/>
|
||||||
|
<rect x="88" y="227" width="195" height="11" rx="3" fill="url(#gQ)" opacity="0.85"/>
|
||||||
|
<text x="30" y="255" fill="#9ece6a" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">Speed</text>
|
||||||
|
<rect x="88" y="245" width="244" height="11" rx="3" fill="#21262d"/>
|
||||||
|
<rect x="88" y="245" width="98" height="11" rx="3" fill="url(#gS)" opacity="0.85"/>
|
||||||
|
<text x="30" y="273" fill="#bb9af7" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">VRAM</text>
|
||||||
|
<rect x="88" y="263" width="244" height="11" rx="3" fill="#21262d"/>
|
||||||
|
<rect x="88" y="263" width="98" height="11" rx="3" fill="url(#gV)" opacity="0.85"/>
|
||||||
|
<text x="272" y="298" fill="#8b949e" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="10">Apache 2.0</text>
|
||||||
|
|
||||||
|
<!-- ═══ GIMM-VFI (bottom-right) ═══ -->
|
||||||
|
<rect x="370" y="165" width="340" height="145" rx="10" fill="#161b22" stroke="#30363d" stroke-width="1"/>
|
||||||
|
<rect x="371" y="177" width="3" height="121" fill="#bc8cff"/>
|
||||||
|
<text x="390" y="193" fill="#e6edf3" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="15" font-weight="600">GIMM-VFI</text>
|
||||||
|
<text x="390" y="211" fill="#bc8cff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">Single-pass 4×/8× · NeurIPS 2024</text>
|
||||||
|
<line x1="390" y1="219" x2="690" y2="219" stroke="#30363d" stroke-width="0.5"/>
|
||||||
|
<text x="390" y="237" fill="#7aa2f7" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">Quality</text>
|
||||||
|
<rect x="448" y="227" width="244" height="11" rx="3" fill="#21262d"/>
|
||||||
|
<rect x="448" y="227" width="146" height="11" rx="3" fill="url(#gQ)" opacity="0.85"/>
|
||||||
|
<text x="390" y="255" fill="#9ece6a" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">Speed</text>
|
||||||
|
<rect x="448" y="245" width="244" height="11" rx="3" fill="#21262d"/>
|
||||||
|
<rect x="448" y="245" width="195" height="11" rx="3" fill="url(#gS)" opacity="0.85"/>
|
||||||
|
<text x="390" y="273" fill="#bb9af7" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">VRAM</text>
|
||||||
|
<rect x="448" y="263" width="244" height="11" rx="3" fill="#21262d"/>
|
||||||
|
<rect x="448" y="263" width="146" height="11" rx="3" fill="url(#gV)" opacity="0.85"/>
|
||||||
|
<text x="632" y="298" fill="#8b949e" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="10">Apache 2.0</text>
|
||||||
|
|
||||||
|
<!-- ═══ SPEED (third-row left) ═══ -->
|
||||||
|
<rect x="10" y="320" width="340" height="145" rx="10" fill="#161b22" stroke="#30363d" stroke-width="1"/>
|
||||||
|
<rect x="11" y="332" width="3" height="121" fill="#f2cc60"/>
|
||||||
|
<text x="30" y="348" fill="#e6edf3" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="15" font-weight="600">SPEED</text>
|
||||||
|
<text x="30" y="366" fill="#f2cc60" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">Modern pairwise default · ACM MM 2026</text>
|
||||||
|
<line x1="30" y1="374" x2="330" y2="374" stroke="#30363d" stroke-width="0.5"/>
|
||||||
|
<text x="30" y="392" fill="#7aa2f7" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">Quality</text>
|
||||||
|
<rect x="88" y="382" width="244" height="11" rx="3" fill="#21262d"/>
|
||||||
|
<rect x="88" y="382" width="220" height="11" rx="3" fill="url(#gQ)" opacity="0.85"/>
|
||||||
|
<text x="30" y="410" fill="#9ece6a" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">Speed</text>
|
||||||
|
<rect x="88" y="400" width="244" height="11" rx="3" fill="#21262d"/>
|
||||||
|
<rect x="88" y="400" width="190" height="11" rx="3" fill="url(#gS)" opacity="0.85"/>
|
||||||
|
<text x="30" y="428" fill="#bb9af7" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">VRAM</text>
|
||||||
|
<rect x="88" y="418" width="244" height="11" rx="3" fill="#21262d"/>
|
||||||
|
<rect x="88" y="418" width="175" height="11" rx="3" fill="url(#gV)" opacity="0.85"/>
|
||||||
|
<text x="253" y="453" fill="#f0883e" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="10">Check upstream terms</text>
|
||||||
|
|
||||||
|
<!-- ═══ LDF-VFI (third-row right) ═══ -->
|
||||||
|
<rect x="370" y="320" width="340" height="145" rx="10" fill="#161b22" stroke="#30363d" stroke-width="1"/>
|
||||||
|
<rect x="371" y="332" width="3" height="121" fill="#ff7b72"/>
|
||||||
|
<text x="390" y="348" fill="#e6edf3" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="15" font-weight="600">LDF-VFI</text>
|
||||||
|
<text x="390" y="366" fill="#ff7b72" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">Long-sequence coherence · CVPR 2026</text>
|
||||||
|
<line x1="390" y1="374" x2="690" y2="374" stroke="#30363d" stroke-width="0.5"/>
|
||||||
|
<text x="390" y="392" fill="#7aa2f7" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">Quality</text>
|
||||||
|
<rect x="448" y="382" width="244" height="11" rx="3" fill="#21262d"/>
|
||||||
|
<rect x="448" y="382" width="230" height="11" rx="3" fill="url(#gQ)" opacity="0.85"/>
|
||||||
|
<text x="390" y="410" fill="#9ece6a" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">Speed</text>
|
||||||
|
<rect x="448" y="400" width="244" height="11" rx="3" fill="#21262d"/>
|
||||||
|
<rect x="448" y="400" width="48" height="11" rx="3" fill="url(#gS)" opacity="0.85"/>
|
||||||
|
<text x="390" y="428" fill="#bb9af7" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="11">VRAM</text>
|
||||||
|
<rect x="448" y="418" width="244" height="11" rx="3" fill="#21262d"/>
|
||||||
|
<rect x="448" y="418" width="40" height="11" rx="3" fill="url(#gV)" opacity="0.85"/>
|
||||||
|
<text x="632" y="453" fill="#8b949e" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','Noto Sans',Helvetica,Arial,sans-serif" font-size="10">Apache 2.0</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 11 KiB |
@@ -12,13 +12,17 @@ from ..utils.padder import InputPadder
|
|||||||
|
|
||||||
|
|
||||||
class BiMVFI(nn.Module):
|
class BiMVFI(nn.Module):
|
||||||
def __init__(self, pyr_level=3, feat_channels=32, **kwargs):
|
def __init__(self, pyr_level=3, feat_channels=32,
|
||||||
|
artifact_safe_mode=False, **kwargs):
|
||||||
super(BiMVFI, self).__init__()
|
super(BiMVFI, self).__init__()
|
||||||
self.pyr_level = pyr_level
|
self.pyr_level = pyr_level
|
||||||
self.mfe = ResNetPyramid(feat_channels)
|
self.mfe = ResNetPyramid(feat_channels)
|
||||||
self.cfe = ResNetPyramid(feat_channels)
|
self.cfe = ResNetPyramid(feat_channels)
|
||||||
self.bimfn = BiMFN(feat_channels)
|
self.bimfn = BiMFN(feat_channels)
|
||||||
self.sn = SynthesisNetwork(feat_channels)
|
self.sn = SynthesisNetwork(
|
||||||
|
feat_channels,
|
||||||
|
use_rgb_refine_residual=not artifact_safe_mode,
|
||||||
|
)
|
||||||
self.feat_channels = feat_channels
|
self.feat_channels = feat_channels
|
||||||
self.caun = CAUN(feat_channels)
|
self.caun = CAUN(feat_channels)
|
||||||
|
|
||||||
|
|||||||
+48
-6
@@ -4,6 +4,7 @@ import collections
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import torch
|
import torch
|
||||||
|
import torch.nn.functional as F
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
cupy = None
|
cupy = None
|
||||||
@@ -14,12 +15,11 @@ def _ensure_cupy():
|
|||||||
try:
|
try:
|
||||||
import cupy as _cupy
|
import cupy as _cupy
|
||||||
cupy = _cupy
|
cupy = _cupy
|
||||||
except ImportError:
|
except Exception:
|
||||||
raise RuntimeError(
|
# Broad catch: an installed-but-broken cupy (e.g. incompatible
|
||||||
"cupy is required for BIM-VFI. Install it with:\n"
|
# NumPy) raises non-ImportError exceptions at import time. Treat any
|
||||||
" pip install cupy-cuda12x (or cupy-cuda11x for CUDA 11)\n"
|
# failure as "cupy unavailable"; the PyTorch fallback will be used.
|
||||||
"Or run install.py from the ComfyUI-Tween directory."
|
pass
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
##########################################################
|
##########################################################
|
||||||
@@ -246,6 +246,44 @@ def cuda_launch(strKey:str):
|
|||||||
# end
|
# end
|
||||||
|
|
||||||
|
|
||||||
|
def _pytorch_costvol_impl(tenOne, tenTwo, intKernelSize):
|
||||||
|
"""Pure-PyTorch local cost volume via unfold + dot product."""
|
||||||
|
B, C, H, W = tenOne.shape
|
||||||
|
pad = (intKernelSize - 1) // 2
|
||||||
|
|
||||||
|
# Pad tenTwo so out-of-bounds yields 0 (matches CUDA kernel)
|
||||||
|
tenTwo_padded = F.pad(tenTwo, [pad, pad, pad, pad])
|
||||||
|
|
||||||
|
# Unfold into patches: (B, C, H, W, K, K)
|
||||||
|
patches = tenTwo_padded.unfold(2, intKernelSize, 1).unfold(3, intKernelSize, 1)
|
||||||
|
# Reshape to (B, C, H, W, K*K)
|
||||||
|
patches = patches.contiguous().view(B, C, H, W, intKernelSize * intKernelSize)
|
||||||
|
|
||||||
|
# Dot product over C dimension: (B, H, W, K*K)
|
||||||
|
tenOut = (tenOne.unsqueeze(-1) * patches).sum(dim=1)
|
||||||
|
|
||||||
|
# Permute to (B, K*K, H, W) to match CUDA output layout
|
||||||
|
tenOut = tenOut.permute(0, 3, 1, 2).contiguous()
|
||||||
|
|
||||||
|
return tenOut
|
||||||
|
|
||||||
|
|
||||||
|
_costvol_fn = None
|
||||||
|
|
||||||
|
def _pytorch_costvol(tenOne, tenTwo, intKernelSize):
|
||||||
|
global _costvol_fn
|
||||||
|
if _costvol_fn is None:
|
||||||
|
try:
|
||||||
|
_costvol_fn = torch.compile(_pytorch_costvol_impl)
|
||||||
|
except Exception:
|
||||||
|
_costvol_fn = _pytorch_costvol_impl
|
||||||
|
try:
|
||||||
|
return _costvol_fn(tenOne, tenTwo, intKernelSize)
|
||||||
|
except Exception:
|
||||||
|
_costvol_fn = _pytorch_costvol_impl
|
||||||
|
return _costvol_fn(tenOne, tenTwo, intKernelSize)
|
||||||
|
|
||||||
|
|
||||||
##########################################################
|
##########################################################
|
||||||
|
|
||||||
|
|
||||||
@@ -253,6 +291,8 @@ class costvol_func(torch.autograd.Function):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
@torch.amp.custom_fwd(device_type='cuda', cast_inputs=torch.float32)
|
@torch.amp.custom_fwd(device_type='cuda', cast_inputs=torch.float32)
|
||||||
def forward(self, tenOne, tenTwo, intKernelSize):
|
def forward(self, tenOne, tenTwo, intKernelSize):
|
||||||
|
_ensure_cupy()
|
||||||
|
if tenOne.is_cuda and cupy is not None:
|
||||||
tenOut = tenOne.new_empty([tenOne.shape[0], intKernelSize ** 2, tenOne.shape[2], tenOne.shape[3]])
|
tenOut = tenOne.new_empty([tenOne.shape[0], intKernelSize ** 2, tenOne.shape[2], tenOne.shape[3]])
|
||||||
|
|
||||||
cuda_launch(cuda_kernel('costvol_out', '''
|
cuda_launch(cuda_kernel('costvol_out', '''
|
||||||
@@ -302,6 +342,8 @@ class costvol_func(torch.autograd.Function):
|
|||||||
args=[cuda_int32(tenOut.shape[0] * tenOut.shape[2] * tenOut.shape[3]), tenOne.data_ptr(), tenTwo.data_ptr(), intKernelSize, tenOut.data_ptr()],
|
args=[cuda_int32(tenOut.shape[0] * tenOut.shape[2] * tenOut.shape[3]), tenOne.data_ptr(), tenTwo.data_ptr(), intKernelSize, tenOut.data_ptr()],
|
||||||
stream=collections.namedtuple('Stream', 'ptr')(torch.cuda.current_stream().cuda_stream)
|
stream=collections.namedtuple('Stream', 'ptr')(torch.cuda.current_stream().cuda_stream)
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
tenOut = _pytorch_costvol(tenOne, tenTwo, intKernelSize)
|
||||||
|
|
||||||
self.save_for_backward(tenOne, tenTwo)
|
self.save_for_backward(tenOne, tenTwo)
|
||||||
self.intKernelSize = intKernelSize
|
self.intKernelSize = intKernelSize
|
||||||
|
|||||||
+12
-2
@@ -5,8 +5,9 @@ from .backwarp import backwarp
|
|||||||
|
|
||||||
|
|
||||||
class SynthesisNetwork(nn.Module):
|
class SynthesisNetwork(nn.Module):
|
||||||
def __init__(self, feat_channels):
|
def __init__(self, feat_channels, use_rgb_refine_residual=True):
|
||||||
super(SynthesisNetwork, self).__init__()
|
super(SynthesisNetwork, self).__init__()
|
||||||
|
self.use_rgb_refine_residual = use_rgb_refine_residual
|
||||||
input_channels = 6 + 1
|
input_channels = 6 + 1
|
||||||
self.conv_down1 = nn.Sequential(
|
self.conv_down1 = nn.Sequential(
|
||||||
nn.Conv2d(input_channels, feat_channels, 7, padding=3),
|
nn.Conv2d(input_channels, feat_channels, 7, padding=3),
|
||||||
@@ -59,6 +60,13 @@ class SynthesisNetwork(nn.Module):
|
|||||||
warped_img1 = backwarp(i1, flow_t1)
|
warped_img1 = backwarp(i1, flow_t1)
|
||||||
return warped_img0, warped_img1, warped_c0, warped_c1
|
return warped_img0, warped_img1, warped_c0, warped_c1
|
||||||
|
|
||||||
|
def merge_warped_images(self, warped_img0, warped_img1,
|
||||||
|
blending_mask, refine_res):
|
||||||
|
merged_img = warped_img0 * blending_mask + warped_img1 * (1 - blending_mask)
|
||||||
|
if self.use_rgb_refine_residual:
|
||||||
|
merged_img = merged_img + refine_res
|
||||||
|
return merged_img
|
||||||
|
|
||||||
def forward(self, i0, i1, c0_pyr, c1_pyr, bi_flow_pyr, occ):
|
def forward(self, i0, i1, c0_pyr, c1_pyr, bi_flow_pyr, occ):
|
||||||
warped_img0, warped_img1, warped_c0, warped_c1 = \
|
warped_img0, warped_img1, warped_c0, warped_c1 = \
|
||||||
self.get_warped_representations(
|
self.get_warped_representations(
|
||||||
@@ -82,7 +90,9 @@ class SynthesisNetwork(nn.Module):
|
|||||||
occ_res = refine[:, 3:]
|
occ_res = refine[:, 3:]
|
||||||
occ_out = occ + occ_res
|
occ_out = occ + occ_res
|
||||||
blending_mask = torch.sigmoid(occ_out)
|
blending_mask = torch.sigmoid(occ_out)
|
||||||
merged_img = (warped_img0 * blending_mask + warped_img1 * (1 - blending_mask)) + refine_res
|
merged_img = self.merge_warped_images(
|
||||||
|
warped_img0, warped_img1, blending_mask, refine_res
|
||||||
|
)
|
||||||
interp_img = merged_img
|
interp_img = merged_img
|
||||||
|
|
||||||
extra_dict = {}
|
extra_dict = {}
|
||||||
|
|||||||
@@ -0,0 +1,297 @@
|
|||||||
|
# Pure-PyTorch Fallbacks for cupy Kernels
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Make BIM-VFI, SGM-VFI, and GIMM-VFI work without cupy by adding pure-PyTorch fallback implementations of softsplat and costvol.
|
||||||
|
|
||||||
|
**Architecture:** Each kernel file (`sgm_vfi_arch/softsplat.py`, `gimm_vfi_arch/.../softsplat.py`, `bim_vfi_arch/costvol.py`) gets a `_pytorch_*` fallback function. The `softsplat_func.forward()` and `costvol_func.forward()` methods dispatch to cupy when available, otherwise use the fallback. The `_check_cupy()` gate in `nodes.py` is removed so models can load on any backend.
|
||||||
|
|
||||||
|
**Tech Stack:** PyTorch (`scatter_add_`, `F.unfold`, `F.pad`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add pure-PyTorch softsplat fallback to SGM-VFI
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `sgm_vfi_arch/softsplat.py`
|
||||||
|
|
||||||
|
**Step 1: Add cupy availability flag and fallback function**
|
||||||
|
|
||||||
|
At the top of `sgm_vfi_arch/softsplat.py`, change the hard `import cupy` to a try/except, and add the fallback function after the `cuda_launch` function (before the `softsplat()` function).
|
||||||
|
|
||||||
|
Replace:
|
||||||
|
```python
|
||||||
|
import cupy
|
||||||
|
```
|
||||||
|
With:
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
import cupy
|
||||||
|
except ImportError:
|
||||||
|
cupy = None
|
||||||
|
```
|
||||||
|
|
||||||
|
Add this fallback function (after `cuda_launch`, before `softsplat`):
|
||||||
|
```python
|
||||||
|
def _pytorch_softsplat(tenIn, tenFlow):
|
||||||
|
B, C, H, W = tenIn.shape
|
||||||
|
tenOut = tenIn.new_zeros(B, C, H, W)
|
||||||
|
|
||||||
|
# Build base grid: (x, y) for each pixel
|
||||||
|
grid_y, grid_x = torch.meshgrid(
|
||||||
|
torch.arange(H, device=tenIn.device, dtype=tenIn.dtype),
|
||||||
|
torch.arange(W, device=tenIn.device, dtype=tenIn.dtype),
|
||||||
|
indexing='ij',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Target positions
|
||||||
|
flt_x = grid_x.unsqueeze(0) + tenFlow[:, 0, :, :] # (B, H, W)
|
||||||
|
flt_y = grid_y.unsqueeze(0) + tenFlow[:, 1, :, :]
|
||||||
|
|
||||||
|
# Filter non-finite
|
||||||
|
valid = torch.isfinite(flt_x) & torch.isfinite(flt_y)
|
||||||
|
flt_x = torch.where(valid, flt_x, torch.zeros_like(flt_x))
|
||||||
|
flt_y = torch.where(valid, flt_y, torch.zeros_like(flt_y))
|
||||||
|
|
||||||
|
# Four neighbors (NW, NE, SW, SE)
|
||||||
|
nw_x = flt_x.floor().long()
|
||||||
|
nw_y = flt_y.floor().long()
|
||||||
|
|
||||||
|
# Bilinear weights
|
||||||
|
frac_x = flt_x - nw_x.float()
|
||||||
|
frac_y = flt_y - nw_y.float()
|
||||||
|
w_nw = (1.0 - frac_x) * (1.0 - frac_y)
|
||||||
|
w_ne = frac_x * (1.0 - frac_y)
|
||||||
|
w_sw = (1.0 - frac_x) * frac_y
|
||||||
|
w_se = frac_x * frac_y
|
||||||
|
|
||||||
|
# Zero out invalid pixels
|
||||||
|
w_nw = w_nw * valid
|
||||||
|
w_ne = w_ne * valid
|
||||||
|
w_sw = w_sw * valid
|
||||||
|
w_se = w_se * valid
|
||||||
|
|
||||||
|
# For each of the 4 neighbors, scatter into output
|
||||||
|
for dx, dy, w in [(0, 0, w_nw), (1, 0, w_ne), (0, 1, w_sw), (1, 1, w_se)]:
|
||||||
|
tx = nw_x + dx
|
||||||
|
ty = nw_y + dy
|
||||||
|
in_bounds = (tx >= 0) & (tx < W) & (ty >= 0) & (ty < H)
|
||||||
|
w_masked = w * in_bounds
|
||||||
|
|
||||||
|
# Flatten to 1D index for scatter_add
|
||||||
|
idx = (ty.clamp(0, H - 1) * W + tx.clamp(0, W - 1)) # (B, H, W)
|
||||||
|
idx = idx.unsqueeze(1).expand_as(tenIn) # (B, C, H, W)
|
||||||
|
weighted = tenIn * w_masked.unsqueeze(1) # (B, C, H, W)
|
||||||
|
tenOut.view(B, C, -1).scatter_add_(2, idx.reshape(B, C, -1), weighted.reshape(B, C, -1))
|
||||||
|
|
||||||
|
return tenOut
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Update softsplat_func.forward to use fallback**
|
||||||
|
|
||||||
|
In `softsplat_func.forward()`, replace the `elif tenIn.is_cuda != True: assert(False)` block so it dispatches to the fallback when cupy is unavailable or when not on CUDA:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Current:
|
||||||
|
if tenIn.is_cuda == True:
|
||||||
|
cuda_launch(cuda_kernel(...))(...)
|
||||||
|
elif tenIn.is_cuda != True:
|
||||||
|
assert(False)
|
||||||
|
|
||||||
|
# New:
|
||||||
|
if tenIn.is_cuda and cupy is not None:
|
||||||
|
cuda_launch(cuda_kernel(...))(...)
|
||||||
|
else:
|
||||||
|
tenOut = _pytorch_softsplat(tenIn, tenFlow)
|
||||||
|
```
|
||||||
|
|
||||||
|
Also guard the `@cupy.memoize` decorator on `cuda_launch`:
|
||||||
|
```python
|
||||||
|
# Current:
|
||||||
|
@cupy.memoize(for_each_device=True)
|
||||||
|
def cuda_launch(strKey:str):
|
||||||
|
|
||||||
|
# New:
|
||||||
|
def cuda_launch(strKey:str):
|
||||||
|
```
|
||||||
|
(The function already has its own dict-based caching via `objCudacache`, and the memoize is redundant anyway. But the real issue is it crashes at import when cupy=None.)
|
||||||
|
|
||||||
|
Wait - actually `cuda_launch` uses `cupy.RawKernel` inside, so it's only ever called on the cupy path. The `@cupy.memoize` decorator is the problem: it runs at import time. Replace it:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Replace @cupy.memoize(for_each_device=True) with a simple cache dict
|
||||||
|
_cuda_launch_cache = {}
|
||||||
|
|
||||||
|
def cuda_launch(strKey:str):
|
||||||
|
if strKey not in _cuda_launch_cache:
|
||||||
|
if 'CUDA_HOME' not in os.environ:
|
||||||
|
os.environ['CUDA_HOME'] = cupy.cuda.get_cuda_path()
|
||||||
|
_cuda_launch_cache[strKey] = cupy.RawKernel(
|
||||||
|
objCudacache[strKey]['strKernel'],
|
||||||
|
objCudacache[strKey]['strFunction'],
|
||||||
|
options=tuple(['-I ' + os.environ['CUDA_HOME'],
|
||||||
|
'-I ' + os.environ['CUDA_HOME'] + '/include'])
|
||||||
|
)
|
||||||
|
return _cuda_launch_cache[strKey]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add sgm_vfi_arch/softsplat.py
|
||||||
|
git commit -m "feat: add pure-PyTorch softsplat fallback for SGM-VFI"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Add pure-PyTorch softsplat fallback to GIMM-VFI
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `gimm_vfi_arch/generalizable_INR/modules/softsplat.py`
|
||||||
|
|
||||||
|
**Step 1: Add cupy availability flag and fallback function**
|
||||||
|
|
||||||
|
Same pattern as Task 1. Replace `import cupy` with try/except. Add the same `_pytorch_softsplat()` function. Replace `@cupy.memoize(for_each_device=True)` on `cuda_launch` with a dict cache.
|
||||||
|
|
||||||
|
The GIMM softsplat.py already has `@torch.compiler.disable()` on `cuda_launch` — keep that decorator.
|
||||||
|
|
||||||
|
**Step 2: Update softsplat_func.forward dispatch**
|
||||||
|
|
||||||
|
Same pattern: if `tenIn.is_cuda and cupy is not None` → cupy path, else → `_pytorch_softsplat`.
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add gimm_vfi_arch/generalizable_INR/modules/softsplat.py
|
||||||
|
git commit -m "feat: add pure-PyTorch softsplat fallback for GIMM-VFI"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Add pure-PyTorch costvol fallback to BIM-VFI
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `bim_vfi_arch/costvol.py`
|
||||||
|
|
||||||
|
**Step 1: Add the fallback function**
|
||||||
|
|
||||||
|
After the existing `cuda_launch` function, add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _pytorch_costvol(tenOne, tenTwo, intKernelSize):
|
||||||
|
B, C, H, W = tenOne.shape
|
||||||
|
pad = (intKernelSize - 1) // 2
|
||||||
|
|
||||||
|
# Pad tenTwo with zeros so out-of-bounds accesses yield 0 (matches CUDA kernel)
|
||||||
|
tenTwo_padded = F.pad(tenTwo, [pad, pad, pad, pad])
|
||||||
|
|
||||||
|
# Unfold into (B, C, K*K, H, W) patches
|
||||||
|
patches = tenTwo_padded.unfold(2, intKernelSize, 1).unfold(3, intKernelSize, 1)
|
||||||
|
# patches shape: (B, C, H, W, K, K)
|
||||||
|
patches = patches.contiguous().view(B, C, H, W, intKernelSize * intKernelSize)
|
||||||
|
# -> (B, C, H, W, K^2)
|
||||||
|
|
||||||
|
# Dot product: sum over C
|
||||||
|
# tenOne: (B, C, H, W) -> (B, C, H, W, 1)
|
||||||
|
tenOut = (tenOne.unsqueeze(-1) * patches).sum(dim=1)
|
||||||
|
# tenOut: (B, H, W, K^2)
|
||||||
|
|
||||||
|
# Permute to (B, K^2, H, W) to match CUDA output layout
|
||||||
|
tenOut = tenOut.permute(0, 3, 1, 2).contiguous()
|
||||||
|
|
||||||
|
return tenOut
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `import torch.nn.functional as F` at the top if not already present.
|
||||||
|
|
||||||
|
**Step 2: Update costvol_func.forward dispatch**
|
||||||
|
|
||||||
|
The current forward unconditionally calls `cuda_launch(cuda_kernel(...))`. Change to:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@staticmethod
|
||||||
|
@torch.amp.custom_fwd(device_type='cuda', cast_inputs=torch.float32)
|
||||||
|
def forward(self, tenOne, tenTwo, intKernelSize):
|
||||||
|
if tenOne.is_cuda and cupy is not None:
|
||||||
|
# existing cupy code (unchanged)
|
||||||
|
tenOut = tenOne.new_empty([tenOne.shape[0], intKernelSize ** 2, tenOne.shape[2], tenOne.shape[3]])
|
||||||
|
cuda_launch(cuda_kernel(...))(...)
|
||||||
|
else:
|
||||||
|
tenOut = _pytorch_costvol(tenOne, tenTwo, intKernelSize)
|
||||||
|
|
||||||
|
self.save_for_backward(tenOne, tenTwo)
|
||||||
|
self.intKernelSize = intKernelSize
|
||||||
|
return tenOut
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add bim_vfi_arch/costvol.py
|
||||||
|
git commit -m "feat: add pure-PyTorch costvol fallback for BIM-VFI"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Remove _check_cupy gate from nodes.py
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `nodes.py`
|
||||||
|
|
||||||
|
**Step 1: Remove the _check_cupy function and all its call sites**
|
||||||
|
|
||||||
|
Delete the `_check_cupy()` function definition (lines 22-41). Remove the three calls:
|
||||||
|
- Line 209: `_check_cupy("BIM-VFI")` (in BIM-VFI load)
|
||||||
|
- Line 1377: `_check_cupy("SGM-VFI")` (in SGM-VFI load)
|
||||||
|
- Line 1804: `_check_cupy("GIMM-VFI")` (in GIMM-VFI load)
|
||||||
|
|
||||||
|
**Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add nodes.py
|
||||||
|
git commit -m "feat: remove cupy requirement gate, models now fallback to pure PyTorch"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Make install.py not force cupy installation
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `install.py`
|
||||||
|
|
||||||
|
**Step 1: Change cupy from required to optional**
|
||||||
|
|
||||||
|
Make cupy a soft dependency — try to install it but don't fail if it can't be installed (ROCm users, no CUDA toolkit, etc.). Change `install()`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def install():
|
||||||
|
# Install core requirements first
|
||||||
|
requirements_path = os.path.join(os.path.dirname(__file__), "requirements.txt")
|
||||||
|
subprocess.check_call([
|
||||||
|
sys.executable, "-m", "pip", "install", "-r", requirements_path
|
||||||
|
])
|
||||||
|
|
||||||
|
# Try to install cupy for NVIDIA users (optional, improves performance)
|
||||||
|
cupy_pkg = get_cupy_package()
|
||||||
|
if cupy_pkg:
|
||||||
|
try:
|
||||||
|
subprocess.check_call([
|
||||||
|
sys.executable, "-m", "pip", "install", cupy_pkg
|
||||||
|
])
|
||||||
|
print(f"[Tween] cupy installed successfully ({cupy_pkg})")
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
print(f"[Tween] WARNING: Could not install {cupy_pkg}. "
|
||||||
|
f"BIM-VFI, SGM-VFI, and GIMM-VFI will use slower PyTorch fallback.")
|
||||||
|
else:
|
||||||
|
print("[Tween] cupy not available (no NVIDIA CUDA). "
|
||||||
|
"BIM-VFI, SGM-VFI, and GIMM-VFI will use PyTorch fallback.")
|
||||||
|
```
|
||||||
|
|
||||||
|
Also stop writing cupy into `requirements.txt` — remove the `update_requirements` call and function.
|
||||||
|
|
||||||
|
**Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add install.py
|
||||||
|
git commit -m "feat: make cupy optional in install.py"
|
||||||
|
```
|
||||||
@@ -206,100 +206,6 @@
|
|||||||
"2"
|
"2"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": 12,
|
|
||||||
"type": "easy forLoopStart",
|
|
||||||
"pos": [
|
|
||||||
-8160,
|
|
||||||
576
|
|
||||||
],
|
|
||||||
"size": [
|
|
||||||
270,
|
|
||||||
138
|
|
||||||
],
|
|
||||||
"flags": {},
|
|
||||||
"order": 6,
|
|
||||||
"mode": 0,
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "initial_value1",
|
|
||||||
"shape": 7,
|
|
||||||
"type": "*",
|
|
||||||
"link": 68
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "total",
|
|
||||||
"type": "INT",
|
|
||||||
"widget": {
|
|
||||||
"name": "total"
|
|
||||||
},
|
|
||||||
"link": 33
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "initial_value2",
|
|
||||||
"type": "*",
|
|
||||||
"link": 44
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "initial_value3",
|
|
||||||
"type": "*",
|
|
||||||
"link": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "flow",
|
|
||||||
"shape": 5,
|
|
||||||
"type": "FLOW_CONTROL",
|
|
||||||
"links": [
|
|
||||||
15
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "index",
|
|
||||||
"type": "INT",
|
|
||||||
"links": [
|
|
||||||
25,
|
|
||||||
26
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "value1",
|
|
||||||
"type": "*",
|
|
||||||
"links": [
|
|
||||||
18
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "value2",
|
|
||||||
"type": "*",
|
|
||||||
"links": [
|
|
||||||
21,
|
|
||||||
64
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "value3",
|
|
||||||
"type": "*",
|
|
||||||
"links": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"cnr_id": "comfyui-easy-use",
|
|
||||||
"ver": "7c470c67d6df44498e52c902173c1ac77cd5bdfd",
|
|
||||||
"Node name for S&R": "easy forLoopStart",
|
|
||||||
"ue_properties": {
|
|
||||||
"widget_ue_connectable": {},
|
|
||||||
"input_ue_unconnectable": {},
|
|
||||||
"version": "7.6.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"widgets_values": [
|
|
||||||
6
|
|
||||||
],
|
|
||||||
"color": "#223",
|
|
||||||
"bgcolor": "#335"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": 13,
|
"id": 13,
|
||||||
"type": "easy forLoopEnd",
|
"type": "easy forLoopEnd",
|
||||||
@@ -371,85 +277,6 @@
|
|||||||
"color": "#223",
|
"color": "#223",
|
||||||
"bgcolor": "#335"
|
"bgcolor": "#335"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": 11,
|
|
||||||
"type": "BIMVFISegmentInterpolate",
|
|
||||||
"pos": [
|
|
||||||
-7584,
|
|
||||||
576
|
|
||||||
],
|
|
||||||
"size": [
|
|
||||||
321.58209228515625,
|
|
||||||
246
|
|
||||||
],
|
|
||||||
"flags": {},
|
|
||||||
"order": 9,
|
|
||||||
"mode": 0,
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "images",
|
|
||||||
"type": "IMAGE",
|
|
||||||
"link": 21
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "model",
|
|
||||||
"type": "BIM_VFI_MODEL",
|
|
||||||
"link": 18
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "segment_index",
|
|
||||||
"type": "INT",
|
|
||||||
"widget": {
|
|
||||||
"name": "segment_index"
|
|
||||||
},
|
|
||||||
"link": 25
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "segment_size",
|
|
||||||
"type": "INT",
|
|
||||||
"widget": {
|
|
||||||
"name": "segment_size"
|
|
||||||
},
|
|
||||||
"link": 35
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "images",
|
|
||||||
"type": "IMAGE",
|
|
||||||
"links": [
|
|
||||||
66
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "model",
|
|
||||||
"type": "BIM_VFI_MODEL",
|
|
||||||
"links": [
|
|
||||||
67
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"aux_id": "Comfyui-BIM-VFI.git",
|
|
||||||
"ver": "7cf7162143eaa5b0939e0e122f80bc956baf65ea",
|
|
||||||
"Node name for S&R": "BIMVFISegmentInterpolate",
|
|
||||||
"ue_properties": {
|
|
||||||
"widget_ue_connectable": {},
|
|
||||||
"input_ue_unconnectable": {},
|
|
||||||
"version": "7.6.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"widgets_values": [
|
|
||||||
2,
|
|
||||||
40,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
500
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"type": "LoadBIMVFIModel",
|
"type": "LoadBIMVFIModel",
|
||||||
@@ -561,7 +388,6 @@
|
|||||||
"video/",
|
"video/",
|
||||||
"tween_sgm",
|
"tween_sgm",
|
||||||
"tween_video_sgm.mp4",
|
"tween_video_sgm.mp4",
|
||||||
true,
|
|
||||||
true
|
true
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -574,7 +400,7 @@
|
|||||||
],
|
],
|
||||||
"size": [
|
"size": [
|
||||||
544,
|
544,
|
||||||
352
|
334
|
||||||
],
|
],
|
||||||
"flags": {},
|
"flags": {},
|
||||||
"order": 10,
|
"order": 10,
|
||||||
@@ -647,11 +473,227 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": 16,
|
||||||
|
"type": "PrimitiveInt",
|
||||||
|
"pos": [
|
||||||
|
-9184,
|
||||||
|
544
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
270,
|
||||||
|
82
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 2,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "INT",
|
||||||
|
"type": "INT",
|
||||||
|
"links": [
|
||||||
|
31,
|
||||||
|
35
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Frames number each loops",
|
||||||
|
"properties": {
|
||||||
|
"cnr_id": "comfy-core",
|
||||||
|
"ver": "0.13.0",
|
||||||
|
"Node name for S&R": "PrimitiveInt",
|
||||||
|
"ue_properties": {
|
||||||
|
"widget_ue_connectable": {},
|
||||||
|
"input_ue_unconnectable": {},
|
||||||
|
"version": "7.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
100,
|
||||||
|
"fixed"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"type": "easy forLoopStart",
|
||||||
|
"pos": [
|
||||||
|
-8160,
|
||||||
|
576
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
270,
|
||||||
|
138
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 6,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "initial_value1",
|
||||||
|
"shape": 7,
|
||||||
|
"type": "*",
|
||||||
|
"link": 68
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "total",
|
||||||
|
"type": "INT",
|
||||||
|
"widget": {
|
||||||
|
"name": "total"
|
||||||
|
},
|
||||||
|
"link": 33
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "initial_value2",
|
||||||
|
"type": "*",
|
||||||
|
"link": 44
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "initial_value3",
|
||||||
|
"type": "*",
|
||||||
|
"link": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "flow",
|
||||||
|
"shape": 5,
|
||||||
|
"type": "FLOW_CONTROL",
|
||||||
|
"links": [
|
||||||
|
15
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index",
|
||||||
|
"type": "INT",
|
||||||
|
"links": [
|
||||||
|
25,
|
||||||
|
26
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "value1",
|
||||||
|
"type": "*",
|
||||||
|
"links": [
|
||||||
|
18
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "value2",
|
||||||
|
"type": "*",
|
||||||
|
"links": [
|
||||||
|
21,
|
||||||
|
64
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "value3",
|
||||||
|
"type": "*",
|
||||||
|
"links": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"cnr_id": "comfyui-easy-use",
|
||||||
|
"ver": "7c470c67d6df44498e52c902173c1ac77cd5bdfd",
|
||||||
|
"Node name for S&R": "easy forLoopStart",
|
||||||
|
"ue_properties": {
|
||||||
|
"widget_ue_connectable": {},
|
||||||
|
"input_ue_unconnectable": {},
|
||||||
|
"version": "7.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
6
|
||||||
|
],
|
||||||
|
"color": "#223",
|
||||||
|
"bgcolor": "#335"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"type": "BIMVFISegmentInterpolate",
|
||||||
|
"pos": [
|
||||||
|
-7584,
|
||||||
|
576
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
321.58209228515625,
|
||||||
|
294
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 9,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "images",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"link": 21
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "model",
|
||||||
|
"type": "BIM_VFI_MODEL",
|
||||||
|
"link": 18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "segment_index",
|
||||||
|
"type": "INT",
|
||||||
|
"widget": {
|
||||||
|
"name": "segment_index"
|
||||||
|
},
|
||||||
|
"link": 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "segment_size",
|
||||||
|
"type": "INT",
|
||||||
|
"widget": {
|
||||||
|
"name": "segment_size"
|
||||||
|
},
|
||||||
|
"link": 35
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "images",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"links": [
|
||||||
|
66
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "model",
|
||||||
|
"type": "BIM_VFI_MODEL",
|
||||||
|
"links": [
|
||||||
|
67
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"aux_id": "Comfyui-BIM-VFI.git",
|
||||||
|
"ver": "7cf7162143eaa5b0939e0e122f80bc956baf65ea",
|
||||||
|
"Node name for S&R": "BIMVFISegmentInterpolate",
|
||||||
|
"ue_properties": {
|
||||||
|
"widget_ue_connectable": {},
|
||||||
|
"input_ue_unconnectable": {},
|
||||||
|
"version": "7.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
2,
|
||||||
|
40,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
1,
|
||||||
|
500,
|
||||||
|
16,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
500
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": 28,
|
"id": 28,
|
||||||
"type": "VHS_LoadVideoPath",
|
"type": "VHS_LoadVideoPath",
|
||||||
"pos": [
|
"pos": [
|
||||||
-9152,
|
-9184,
|
||||||
704
|
704
|
||||||
],
|
],
|
||||||
"size": [
|
"size": [
|
||||||
@@ -659,7 +701,7 @@
|
|||||||
286
|
286
|
||||||
],
|
],
|
||||||
"flags": {},
|
"flags": {},
|
||||||
"order": 2,
|
"order": 3,
|
||||||
"mode": 0,
|
"mode": 0,
|
||||||
"inputs": [
|
"inputs": [
|
||||||
{
|
{
|
||||||
@@ -738,47 +780,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 16,
|
|
||||||
"type": "PrimitiveInt",
|
|
||||||
"pos": [
|
|
||||||
-9152,
|
|
||||||
576
|
|
||||||
],
|
|
||||||
"size": [
|
|
||||||
270,
|
|
||||||
82
|
|
||||||
],
|
|
||||||
"flags": {},
|
|
||||||
"order": 3,
|
|
||||||
"mode": 0,
|
|
||||||
"inputs": [],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "INT",
|
|
||||||
"type": "INT",
|
|
||||||
"links": [
|
|
||||||
31,
|
|
||||||
35
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Frames number each loops",
|
|
||||||
"properties": {
|
|
||||||
"cnr_id": "comfy-core",
|
|
||||||
"ver": "0.13.0",
|
|
||||||
"Node name for S&R": "PrimitiveInt",
|
|
||||||
"ue_properties": {
|
|
||||||
"widget_ue_connectable": {},
|
|
||||||
"input_ue_unconnectable": {},
|
|
||||||
"version": "7.6.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"widgets_values": [
|
|
||||||
100,
|
|
||||||
"fixed"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [
|
"links": [
|
||||||
@@ -933,10 +934,10 @@
|
|||||||
"workflowRendererVersion": "LG",
|
"workflowRendererVersion": "LG",
|
||||||
"ue_links": [],
|
"ue_links": [],
|
||||||
"ds": {
|
"ds": {
|
||||||
"scale": 1.0834705943388552,
|
"scale": 0.8954302432552531,
|
||||||
"offset": [
|
"offset": [
|
||||||
10009.878269742538,
|
10389.297857289295,
|
||||||
-100.68482917709798
|
79.21414284327875
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"links_added_by_ue": [],
|
"links_added_by_ue": [],
|
||||||
|
|||||||
@@ -0,0 +1,323 @@
|
|||||||
|
{
|
||||||
|
"last_node_id": 9,
|
||||||
|
"last_link_id": 14,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "VHS_LoadVideoPath",
|
||||||
|
"pos": [20, 170],
|
||||||
|
"size": [300, 310],
|
||||||
|
"flags": {},
|
||||||
|
"order": 0,
|
||||||
|
"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": [1, 8]},
|
||||||
|
{"name": "frame_count", "type": "INT", "links": null},
|
||||||
|
{"name": "audio", "type": "AUDIO", "links": [7, 14]},
|
||||||
|
{"name": "video_info", "type": "VHS_VIDEOINFO", "links": null}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"cnr_id": "comfyui-videohelpersuite",
|
||||||
|
"Node name for S&R": "VHS_LoadVideoPath"
|
||||||
|
},
|
||||||
|
"widgets_values": {
|
||||||
|
"video": "",
|
||||||
|
"force_rate": 24,
|
||||||
|
"custom_width": 0,
|
||||||
|
"custom_height": 0,
|
||||||
|
"frame_load_cap": 25,
|
||||||
|
"skip_first_frames": 0,
|
||||||
|
"select_every_nth": 1,
|
||||||
|
"format": "AnimateDiff",
|
||||||
|
"videopreview": {
|
||||||
|
"hidden": false,
|
||||||
|
"paused": false,
|
||||||
|
"params": {
|
||||||
|
"filename": "",
|
||||||
|
"type": "path",
|
||||||
|
"format": "video/",
|
||||||
|
"force_rate": 24,
|
||||||
|
"custom_width": 0,
|
||||||
|
"custom_height": 0,
|
||||||
|
"frame_load_cap": 25,
|
||||||
|
"skip_first_frames": 0,
|
||||||
|
"select_every_nth": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "LoadSPEEDVFIModel",
|
||||||
|
"pos": [390, 150],
|
||||||
|
"size": [300, 105],
|
||||||
|
"flags": {},
|
||||||
|
"order": 1,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": [
|
||||||
|
{"name": "model", "type": "SPEED_VFI_MODEL", "links": [2, 4]}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"aux_id": "ComfyUI-Tween.git",
|
||||||
|
"Node name for S&R": "LoadSPEEDVFIModel"
|
||||||
|
},
|
||||||
|
"widgets_values": ["speed.pt", "auto"],
|
||||||
|
"color": "#243b32",
|
||||||
|
"bgcolor": "#315244"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"type": "VFIOptimizer",
|
||||||
|
"pos": [390, 320],
|
||||||
|
"size": [310, 150],
|
||||||
|
"flags": {},
|
||||||
|
"order": 2,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{"name": "images", "type": "IMAGE", "link": 1},
|
||||||
|
{"name": "model", "type": "*", "link": 2}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{"name": "images", "type": "IMAGE", "links": [3]},
|
||||||
|
{"name": "settings", "type": "VFI_SETTINGS", "links": [5]}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"aux_id": "ComfyUI-Tween.git",
|
||||||
|
"Node name for S&R": "VFIOptimizer"
|
||||||
|
},
|
||||||
|
"widgets_values": [2, 0],
|
||||||
|
"color": "#243b32",
|
||||||
|
"bgcolor": "#315244"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"type": "SPEEDVFIInterpolate",
|
||||||
|
"pos": [760, 220],
|
||||||
|
"size": [355, 360],
|
||||||
|
"flags": {},
|
||||||
|
"order": 3,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{"name": "images", "type": "IMAGE", "link": 3},
|
||||||
|
{"name": "model", "type": "SPEED_VFI_MODEL", "link": 4},
|
||||||
|
{"name": "settings", "shape": 7, "type": "VFI_SETTINGS", "link": 5}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{"name": "images", "type": "IMAGE", "links": [6]},
|
||||||
|
{"name": "oversampled", "type": "IMAGE", "links": null},
|
||||||
|
{"name": "elapsed_seconds", "type": "FLOAT", "links": null}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"aux_id": "ComfyUI-Tween.git",
|
||||||
|
"Node name for S&R": "SPEEDVFIInterpolate"
|
||||||
|
},
|
||||||
|
"widgets_values": [2, 10, true, false, 1, 0, 24, 48, 0],
|
||||||
|
"color": "#243b32",
|
||||||
|
"bgcolor": "#315244"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "VHS_VideoCombine",
|
||||||
|
"pos": [1170, 220],
|
||||||
|
"size": [360, 340],
|
||||||
|
"flags": {},
|
||||||
|
"order": 4,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{"name": "images", "type": "IMAGE", "link": 6},
|
||||||
|
{"name": "audio", "shape": 7, "type": "AUDIO", "link": 7},
|
||||||
|
{"name": "meta_batch", "shape": 7, "type": "VHS_BatchManager", "link": null},
|
||||||
|
{"name": "vae", "shape": 7, "type": "VAE", "link": null}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{"name": "Filenames", "type": "VHS_FILENAMES", "links": null}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"cnr_id": "comfyui-videohelpersuite",
|
||||||
|
"Node name for S&R": "VHS_VideoCombine"
|
||||||
|
},
|
||||||
|
"widgets_values": {
|
||||||
|
"frame_rate": 48,
|
||||||
|
"loop_count": 0,
|
||||||
|
"filename_prefix": "Tween/demo_speed_24_to_48",
|
||||||
|
"format": "video/h264-mp4",
|
||||||
|
"pix_fmt": "yuv420p",
|
||||||
|
"crf": 19,
|
||||||
|
"save_metadata": true,
|
||||||
|
"trim_to_audio": false,
|
||||||
|
"pingpong": false,
|
||||||
|
"save_output": true,
|
||||||
|
"videopreview": {"hidden": false, "paused": false, "params": {}}
|
||||||
|
},
|
||||||
|
"color": "#243b32",
|
||||||
|
"bgcolor": "#315244"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"type": "LoadBIMVFIModel",
|
||||||
|
"pos": [390, 790],
|
||||||
|
"size": [300, 150],
|
||||||
|
"flags": {},
|
||||||
|
"order": 5,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": [
|
||||||
|
{"name": "model", "type": "BIM_VFI_MODEL", "links": [9, 11]}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"aux_id": "ComfyUI-Tween.git",
|
||||||
|
"Node name for S&R": "LoadBIMVFIModel"
|
||||||
|
},
|
||||||
|
"widgets_values": ["bim_vfi.pth", true, 3, false],
|
||||||
|
"color": "#28384a",
|
||||||
|
"bgcolor": "#36506b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"type": "VFIOptimizer",
|
||||||
|
"pos": [390, 970],
|
||||||
|
"size": [310, 150],
|
||||||
|
"flags": {},
|
||||||
|
"order": 6,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{"name": "images", "type": "IMAGE", "link": 8},
|
||||||
|
{"name": "model", "type": "*", "link": 9}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{"name": "images", "type": "IMAGE", "links": [10]},
|
||||||
|
{"name": "settings", "type": "VFI_SETTINGS", "links": [12]}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"aux_id": "ComfyUI-Tween.git",
|
||||||
|
"Node name for S&R": "VFIOptimizer"
|
||||||
|
},
|
||||||
|
"widgets_values": [2, 0],
|
||||||
|
"color": "#28384a",
|
||||||
|
"bgcolor": "#36506b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"type": "BIMVFIInterpolate",
|
||||||
|
"pos": [760, 860],
|
||||||
|
"size": [355, 340],
|
||||||
|
"flags": {},
|
||||||
|
"order": 7,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{"name": "images", "type": "IMAGE", "link": 10},
|
||||||
|
{"name": "model", "type": "BIM_VFI_MODEL", "link": 11},
|
||||||
|
{"name": "settings", "shape": 7, "type": "VFI_SETTINGS", "link": 12}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{"name": "images", "type": "IMAGE", "links": [13]},
|
||||||
|
{"name": "oversampled", "type": "IMAGE", "links": null},
|
||||||
|
{"name": "elapsed_seconds", "type": "FLOAT", "links": null}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"aux_id": "ComfyUI-Tween.git",
|
||||||
|
"Node name for S&R": "BIMVFIInterpolate"
|
||||||
|
},
|
||||||
|
"widgets_values": [2, 10, true, false, 1, 0, 24, 48],
|
||||||
|
"color": "#28384a",
|
||||||
|
"bgcolor": "#36506b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"type": "VHS_VideoCombine",
|
||||||
|
"pos": [1170, 860],
|
||||||
|
"size": [360, 340],
|
||||||
|
"flags": {},
|
||||||
|
"order": 8,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{"name": "images", "type": "IMAGE", "link": 13},
|
||||||
|
{"name": "audio", "shape": 7, "type": "AUDIO", "link": 14},
|
||||||
|
{"name": "meta_batch", "shape": 7, "type": "VHS_BatchManager", "link": null},
|
||||||
|
{"name": "vae", "shape": 7, "type": "VAE", "link": null}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{"name": "Filenames", "type": "VHS_FILENAMES", "links": null}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"cnr_id": "comfyui-videohelpersuite",
|
||||||
|
"Node name for S&R": "VHS_VideoCombine"
|
||||||
|
},
|
||||||
|
"widgets_values": {
|
||||||
|
"frame_rate": 48,
|
||||||
|
"loop_count": 0,
|
||||||
|
"filename_prefix": "Tween/demo_bim_24_to_48",
|
||||||
|
"format": "video/h264-mp4",
|
||||||
|
"pix_fmt": "yuv420p",
|
||||||
|
"crf": 19,
|
||||||
|
"save_metadata": true,
|
||||||
|
"trim_to_audio": false,
|
||||||
|
"pingpong": false,
|
||||||
|
"save_output": true,
|
||||||
|
"videopreview": {"hidden": false, "paused": false, "params": {}}
|
||||||
|
},
|
||||||
|
"color": "#28384a",
|
||||||
|
"bgcolor": "#36506b"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
[1, 1, 0, 3, 0, "IMAGE"],
|
||||||
|
[2, 2, 0, 3, 1, "*"],
|
||||||
|
[3, 3, 0, 4, 0, "IMAGE"],
|
||||||
|
[4, 2, 0, 4, 1, "SPEED_VFI_MODEL"],
|
||||||
|
[5, 3, 1, 4, 2, "VFI_SETTINGS"],
|
||||||
|
[6, 4, 0, 5, 0, "IMAGE"],
|
||||||
|
[7, 1, 2, 5, 1, "AUDIO"],
|
||||||
|
[8, 1, 0, 7, 0, "IMAGE"],
|
||||||
|
[9, 6, 0, 7, 1, "*"],
|
||||||
|
[10, 7, 0, 8, 0, "IMAGE"],
|
||||||
|
[11, 6, 0, 8, 1, "BIM_VFI_MODEL"],
|
||||||
|
[12, 7, 1, 8, 2, "VFI_SETTINGS"],
|
||||||
|
[13, 8, 0, 9, 0, "IMAGE"],
|
||||||
|
[14, 1, 2, 9, 1, "AUDIO"]
|
||||||
|
],
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "INPUT · 24 FPS / 25 FRAMES",
|
||||||
|
"bounding": [-20, 100, 360, 470],
|
||||||
|
"color": "#625d52",
|
||||||
|
"font_size": 22,
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "SPEED",
|
||||||
|
"bounding": [350, 100, 1240, 520],
|
||||||
|
"color": "#347a56",
|
||||||
|
"font_size": 22,
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"title": "BIM-VFI",
|
||||||
|
"bounding": [350, 740, 1240, 510],
|
||||||
|
"color": "#3f6488",
|
||||||
|
"font_size": 22,
|
||||||
|
"flags": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"config": {},
|
||||||
|
"extra": {
|
||||||
|
"workflowRendererVersion": "LG",
|
||||||
|
"ue_links": [],
|
||||||
|
"links_added_by_ue": [],
|
||||||
|
"ds": {"scale": 0.76, "offset": [80, 80]},
|
||||||
|
"frontendVersion": "1.45.19",
|
||||||
|
"VHS_latentpreview": true,
|
||||||
|
"VHS_latentpreviewrate": 0,
|
||||||
|
"VHS_MetadataImage": true,
|
||||||
|
"VHS_KeepIntermediate": true
|
||||||
|
},
|
||||||
|
"version": 0.4
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
"""Pinned, lazy installers for optional upstream model runtimes.
|
||||||
|
|
||||||
|
Tween does not redistribute these projects. Their official source archives are
|
||||||
|
downloaded only when a corresponding loader node is executed, verified against
|
||||||
|
a pinned SHA-256 digest, and kept next to that model's checkpoints.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger("Tween")
|
||||||
|
|
||||||
|
|
||||||
|
UPSTREAM_SOURCES = {
|
||||||
|
"speed": {
|
||||||
|
"project": "SPEED",
|
||||||
|
"commit": "40fadbe85c88cc6e4015062389da464fd7e85ab9",
|
||||||
|
"url": (
|
||||||
|
"https://codeload.github.com/bbldCVer/SPEED/zip/"
|
||||||
|
"40fadbe85c88cc6e4015062389da464fd7e85ab9"
|
||||||
|
),
|
||||||
|
"sha256": "9e9cc71bfeaf7a62008950b8f234f5f035df27b65a5fc0464caee2542f47f68c",
|
||||||
|
"required": "src/models/model.py",
|
||||||
|
},
|
||||||
|
"ldf": {
|
||||||
|
"project": "LDF-VFI",
|
||||||
|
"commit": "61b34d2379df8a313e8e4cb467cc2f74c52b45d7",
|
||||||
|
"url": (
|
||||||
|
"https://codeload.github.com/xypeng9903/LDF-VFI/zip/"
|
||||||
|
"61b34d2379df8a313e8e4cb467cc2f74c52b45d7"
|
||||||
|
),
|
||||||
|
"sha256": "3a903aeb5353c7e5eb932f129d975d1750246502937d8f7b283b61a269c23668",
|
||||||
|
"required": "training/models/precond.py",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_SOURCE_LOCKS = {name: threading.Lock() for name in UPSTREAM_SOURCES}
|
||||||
|
|
||||||
|
|
||||||
|
def _download(url: str, destination: Path) -> None:
|
||||||
|
request = urllib.request.Request(url, headers={"User-Agent": "ComfyUI-Tween"})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, timeout=60) as response, destination.open("wb") as output:
|
||||||
|
shutil.copyfileobj(response, output, length=1024 * 1024)
|
||||||
|
except (OSError, urllib.error.URLError) as exc:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Could not download optional upstream runtime from {url}. "
|
||||||
|
"Check network access and retry the loader node."
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as stream:
|
||||||
|
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_extract(archive: Path, destination: Path) -> Path:
|
||||||
|
with zipfile.ZipFile(archive) as source_zip:
|
||||||
|
members = source_zip.infolist()
|
||||||
|
if not members:
|
||||||
|
raise RuntimeError(f"Downloaded source archive is empty: {archive}")
|
||||||
|
|
||||||
|
destination_resolved = destination.resolve()
|
||||||
|
for member in members:
|
||||||
|
member_path = (destination / member.filename).resolve()
|
||||||
|
if os.path.commonpath((destination_resolved, member_path)) != str(destination_resolved):
|
||||||
|
raise RuntimeError(f"Unsafe path in source archive: {member.filename}")
|
||||||
|
source_zip.extractall(destination)
|
||||||
|
|
||||||
|
top_level = {Path(member.filename).parts[0] for member in members if member.filename}
|
||||||
|
if len(top_level) != 1:
|
||||||
|
raise RuntimeError("Expected one top-level directory in the upstream source archive")
|
||||||
|
return destination / top_level.pop()
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_upstream_source_unlocked(name: str, model_dir: str | os.PathLike[str]) -> str:
|
||||||
|
try:
|
||||||
|
spec = UPSTREAM_SOURCES[name]
|
||||||
|
except KeyError as exc:
|
||||||
|
raise ValueError(f"Unknown Tween upstream source: {name}") from exc
|
||||||
|
|
||||||
|
model_root = Path(model_dir)
|
||||||
|
source_dir = model_root / "_upstream"
|
||||||
|
required_file = source_dir / spec["required"]
|
||||||
|
if required_file.is_file():
|
||||||
|
marker_path = source_dir / ".tween-source.json"
|
||||||
|
if marker_path.is_file():
|
||||||
|
try:
|
||||||
|
marker = json.loads(marker_path.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, json.JSONDecodeError) as exc:
|
||||||
|
raise RuntimeError(f"Invalid source marker: {marker_path}") from exc
|
||||||
|
if (
|
||||||
|
marker.get("commit") != spec["commit"]
|
||||||
|
or marker.get("archive_sha256") != spec["sha256"]
|
||||||
|
):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{spec['project']} runtime at {source_dir} is pinned to a different commit. "
|
||||||
|
"Remove _upstream and run the loader again."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"Using manually installed %s runtime at %s (no Tween verification marker)",
|
||||||
|
spec["project"], source_dir,
|
||||||
|
)
|
||||||
|
return str(source_dir)
|
||||||
|
|
||||||
|
if source_dir.exists():
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Incomplete {spec['project']} runtime at {source_dir}. "
|
||||||
|
"Remove that _upstream directory and run the loader again."
|
||||||
|
)
|
||||||
|
|
||||||
|
model_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
logger.info(
|
||||||
|
"Downloading pinned %s runtime (%s) to %s",
|
||||||
|
spec["project"], spec["commit"][:12], source_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="tween-source-", dir=model_root) as temp_name:
|
||||||
|
temp_dir = Path(temp_name)
|
||||||
|
archive = temp_dir / "source.zip"
|
||||||
|
_download(spec["url"], archive)
|
||||||
|
|
||||||
|
actual_digest = _sha256(archive)
|
||||||
|
if actual_digest != spec["sha256"]:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Checksum mismatch for {spec['project']} source archive: "
|
||||||
|
f"expected {spec['sha256']}, got {actual_digest}"
|
||||||
|
)
|
||||||
|
|
||||||
|
extracted = _safe_extract(archive, temp_dir / "extract")
|
||||||
|
if not (extracted / spec["required"]).is_file():
|
||||||
|
raise RuntimeError(
|
||||||
|
f"The {spec['project']} archive does not contain {spec['required']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
marker = {
|
||||||
|
"project": spec["project"],
|
||||||
|
"commit": spec["commit"],
|
||||||
|
"archive_sha256": spec["sha256"],
|
||||||
|
"source_url": spec["url"],
|
||||||
|
}
|
||||||
|
(extracted / ".tween-source.json").write_text(
|
||||||
|
json.dumps(marker, indent=2) + "\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
shutil.move(str(extracted), str(source_dir))
|
||||||
|
|
||||||
|
logger.info("Installed %s runtime at %s", spec["project"], source_dir)
|
||||||
|
return str(source_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_upstream_source(name: str, model_dir: str | os.PathLike[str]) -> str:
|
||||||
|
"""Return a verified upstream checkout, downloading it once per process."""
|
||||||
|
try:
|
||||||
|
source_lock = _SOURCE_LOCKS[name]
|
||||||
|
except KeyError as exc:
|
||||||
|
raise ValueError(f"Unknown Tween upstream source: {name}") from exc
|
||||||
|
with source_lock:
|
||||||
|
return _ensure_upstream_source_unlocked(name, model_dir)
|
||||||
@@ -9,7 +9,13 @@
|
|||||||
# --------------------------------------------------------
|
# --------------------------------------------------------
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
|
try:
|
||||||
import cupy
|
import cupy
|
||||||
|
except Exception:
|
||||||
|
# Broad catch: an installed-but-broken cupy (e.g. incompatible NumPy)
|
||||||
|
# raises non-ImportError exceptions at import time. Treat any failure as
|
||||||
|
# "cupy unavailable" and fall back to the pure-PyTorch implementation.
|
||||||
|
cupy = None
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import torch
|
import torch
|
||||||
@@ -260,31 +266,94 @@ def cuda_kernel(strFunction: str, strKernel: str, objVariables: typing.Dict):
|
|||||||
# end
|
# end
|
||||||
|
|
||||||
|
|
||||||
@cupy.memoize(for_each_device=True)
|
_cuda_launch_cache = {}
|
||||||
|
|
||||||
@torch.compiler.disable()
|
@torch.compiler.disable()
|
||||||
def cuda_launch(strKey: str):
|
def cuda_launch(strKey: str):
|
||||||
try:
|
if strKey not in _cuda_launch_cache:
|
||||||
os.environ.setdefault("CUDA_HOME", cupy.cuda.get_cuda_path())
|
|
||||||
except Exception:
|
|
||||||
if "CUDA_HOME" not in os.environ:
|
if "CUDA_HOME" not in os.environ:
|
||||||
raise RuntimeError("'CUDA_HOME' not set, unable to find cuda-toolkit installation.")
|
try:
|
||||||
|
cuda_path = cupy.cuda.get_cuda_path()
|
||||||
|
except Exception:
|
||||||
|
cuda_path = None
|
||||||
|
if cuda_path is None:
|
||||||
|
cuda_path = "/usr/local/cuda"
|
||||||
|
os.environ["CUDA_HOME"] = cuda_path
|
||||||
strKernel = objCudacache[strKey]["strKernel"]
|
strKernel = objCudacache[strKey]["strKernel"]
|
||||||
strFunction = objCudacache[strKey]["strFunction"]
|
strFunction = objCudacache[strKey]["strFunction"]
|
||||||
|
_cuda_launch_cache[strKey] = cupy.RawModule(
|
||||||
return cupy.RawModule(
|
|
||||||
code=strKernel,
|
code=strKernel,
|
||||||
options=(
|
options=(
|
||||||
"-I " + os.environ["CUDA_HOME"],
|
"-I " + os.environ["CUDA_HOME"],
|
||||||
"-I " + os.environ["CUDA_HOME"] + "/include",
|
"-I " + os.environ["CUDA_HOME"] + "/include",
|
||||||
),
|
),
|
||||||
).get_function(strFunction)
|
).get_function(strFunction)
|
||||||
|
return _cuda_launch_cache[strKey]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##########################################################
|
##########################################################
|
||||||
|
|
||||||
|
|
||||||
|
def _pytorch_softsplat_impl(tenIn, tenFlow):
|
||||||
|
"""Pure-PyTorch forward warp via bilinear splatting (scatter_add)."""
|
||||||
|
B, C, H, W = tenIn.shape
|
||||||
|
tenOut = tenIn.new_zeros(B, C, H, W)
|
||||||
|
|
||||||
|
grid_y, grid_x = torch.meshgrid(
|
||||||
|
torch.arange(H, device=tenIn.device, dtype=tenIn.dtype),
|
||||||
|
torch.arange(W, device=tenIn.device, dtype=tenIn.dtype),
|
||||||
|
indexing='ij',
|
||||||
|
)
|
||||||
|
|
||||||
|
flt_x = grid_x.unsqueeze(0) + tenFlow[:, 0, :, :]
|
||||||
|
flt_y = grid_y.unsqueeze(0) + tenFlow[:, 1, :, :]
|
||||||
|
|
||||||
|
valid = torch.isfinite(flt_x) & torch.isfinite(flt_y)
|
||||||
|
flt_x = torch.where(valid, flt_x, torch.zeros_like(flt_x))
|
||||||
|
flt_y = torch.where(valid, flt_y, torch.zeros_like(flt_y))
|
||||||
|
|
||||||
|
nw_x = flt_x.floor().long()
|
||||||
|
nw_y = flt_y.floor().long()
|
||||||
|
frac_x = flt_x - nw_x.to(flt_x.dtype)
|
||||||
|
frac_y = flt_y - nw_y.to(flt_y.dtype)
|
||||||
|
|
||||||
|
w_nw = (1.0 - frac_x) * (1.0 - frac_y) * valid
|
||||||
|
w_ne = frac_x * (1.0 - frac_y) * valid
|
||||||
|
w_sw = (1.0 - frac_x) * frac_y * valid
|
||||||
|
w_se = frac_x * frac_y * valid
|
||||||
|
|
||||||
|
out_flat = tenOut.view(B, C, -1)
|
||||||
|
|
||||||
|
for dx, dy, w in [(0, 0, w_nw), (1, 0, w_ne), (0, 1, w_sw), (1, 1, w_se)]:
|
||||||
|
tx = nw_x + dx
|
||||||
|
ty = nw_y + dy
|
||||||
|
in_bounds = (tx >= 0) & (tx < W) & (ty >= 0) & (ty < H)
|
||||||
|
w_masked = w * in_bounds
|
||||||
|
idx = (ty.clamp(0, H - 1) * W + tx.clamp(0, W - 1))
|
||||||
|
idx = idx.unsqueeze(1).expand_as(tenIn)
|
||||||
|
weighted = tenIn * w_masked.unsqueeze(1)
|
||||||
|
out_flat.scatter_add_(2, idx.reshape(B, C, -1), weighted.reshape(B, C, -1))
|
||||||
|
|
||||||
|
return tenOut
|
||||||
|
|
||||||
|
|
||||||
|
_softsplat_fn = None
|
||||||
|
|
||||||
|
def _pytorch_softsplat(tenIn, tenFlow):
|
||||||
|
global _softsplat_fn
|
||||||
|
if _softsplat_fn is None:
|
||||||
|
try:
|
||||||
|
_softsplat_fn = torch.compile(_pytorch_softsplat_impl)
|
||||||
|
except Exception:
|
||||||
|
_softsplat_fn = _pytorch_softsplat_impl
|
||||||
|
try:
|
||||||
|
return _softsplat_fn(tenIn, tenFlow)
|
||||||
|
except Exception:
|
||||||
|
_softsplat_fn = _pytorch_softsplat_impl
|
||||||
|
return _softsplat_fn(tenIn, tenFlow)
|
||||||
|
|
||||||
|
|
||||||
@torch.compiler.disable()
|
@torch.compiler.disable()
|
||||||
def softsplat(tenIn, tenFlow, tenMetric, strMode, return_norm=False):
|
def softsplat(tenIn, tenFlow, tenMetric, strMode, return_norm=False):
|
||||||
assert strMode.split("-")[0] in ["sum", "avg", "linear", "softmax"]
|
assert strMode.split("-")[0] in ["sum", "avg", "linear", "softmax"]
|
||||||
@@ -366,7 +435,7 @@ class softsplat_func(torch.autograd.Function):
|
|||||||
[tenIn.shape[0], tenIn.shape[1], tenIn.shape[2], tenIn.shape[3]]
|
[tenIn.shape[0], tenIn.shape[1], tenIn.shape[2], tenIn.shape[3]]
|
||||||
)
|
)
|
||||||
|
|
||||||
if tenIn.is_cuda == True:
|
if tenIn.is_cuda and cupy is not None:
|
||||||
cuda_launch(
|
cuda_launch(
|
||||||
cuda_kernel(
|
cuda_kernel(
|
||||||
"softsplat_out",
|
"softsplat_out",
|
||||||
@@ -439,8 +508,8 @@ class softsplat_func(torch.autograd.Function):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
elif tenIn.is_cuda != True:
|
else:
|
||||||
assert False
|
tenOut = _pytorch_softsplat(tenIn, tenFlow)
|
||||||
|
|
||||||
# end
|
# end
|
||||||
|
|
||||||
|
|||||||
+12
-5
@@ -17,12 +17,18 @@ logger = logging.getLogger("Tween")
|
|||||||
class BiMVFIModel:
|
class BiMVFIModel:
|
||||||
"""Clean inference wrapper around BiMVFI for ComfyUI integration."""
|
"""Clean inference wrapper around BiMVFI for ComfyUI integration."""
|
||||||
|
|
||||||
def __init__(self, checkpoint_path, pyr_level=3, auto_pyr_level=True, device="cpu"):
|
def __init__(self, checkpoint_path, pyr_level=3, auto_pyr_level=True,
|
||||||
|
artifact_safe_mode=False, device="cpu"):
|
||||||
self.pyr_level = pyr_level
|
self.pyr_level = pyr_level
|
||||||
self.auto_pyr_level = auto_pyr_level
|
self.auto_pyr_level = auto_pyr_level
|
||||||
|
self.artifact_safe_mode = artifact_safe_mode
|
||||||
self.device = device
|
self.device = device
|
||||||
|
|
||||||
self.model = BiMVFI(pyr_level=pyr_level, feat_channels=32)
|
self.model = BiMVFI(
|
||||||
|
pyr_level=pyr_level,
|
||||||
|
feat_channels=32,
|
||||||
|
artifact_safe_mode=artifact_safe_mode,
|
||||||
|
)
|
||||||
self._load_checkpoint(checkpoint_path)
|
self._load_checkpoint(checkpoint_path)
|
||||||
self.model.eval()
|
self.model.eval()
|
||||||
self.model.to(device)
|
self.model.to(device)
|
||||||
@@ -61,10 +67,11 @@ class BiMVFIModel:
|
|||||||
return 7
|
return 7
|
||||||
elif h >= 1080:
|
elif h >= 1080:
|
||||||
return 6
|
return 6
|
||||||
elif h >= 540:
|
|
||||||
return 5
|
|
||||||
else:
|
else:
|
||||||
return 3
|
# Match the official video inference path. Level 3 can miss
|
||||||
|
# large motion even at low resolutions; it remains available
|
||||||
|
# as a manual speed/quality tradeoff.
|
||||||
|
return 5
|
||||||
return self.pyr_level
|
return self.pyr_level
|
||||||
|
|
||||||
@torch.no_grad()
|
@torch.no_grad()
|
||||||
|
|||||||
-51
@@ -1,51 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
def get_cupy_package():
|
|
||||||
"""Detect PyTorch's CUDA version and return the matching cupy package name."""
|
|
||||||
try:
|
|
||||||
import torch
|
|
||||||
if not torch.cuda.is_available():
|
|
||||||
print("[Tween] WARNING: CUDA not available. cupy requires CUDA.")
|
|
||||||
return None
|
|
||||||
cuda_version = torch.version.cuda
|
|
||||||
if cuda_version is None:
|
|
||||||
print("[Tween] WARNING: PyTorch has no CUDA version info.")
|
|
||||||
return None
|
|
||||||
major = int(cuda_version.split(".")[0])
|
|
||||||
cupy_pkg = f"cupy-cuda{major}x"
|
|
||||||
print(f"[Tween] Detected CUDA {cuda_version}, will use {cupy_pkg}")
|
|
||||||
return cupy_pkg
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[Tween] WARNING: Could not detect CUDA version: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def update_requirements(cupy_pkg):
|
|
||||||
"""Write the correct cupy package into requirements.txt."""
|
|
||||||
requirements_path = os.path.join(os.path.dirname(__file__), "requirements.txt")
|
|
||||||
lines = []
|
|
||||||
if os.path.exists(requirements_path):
|
|
||||||
with open(requirements_path, "r") as f:
|
|
||||||
lines = [l.rstrip() for l in f if not l.strip().startswith("cupy")]
|
|
||||||
if cupy_pkg and cupy_pkg not in lines:
|
|
||||||
lines.append(cupy_pkg)
|
|
||||||
with open(requirements_path, "w") as f:
|
|
||||||
f.write("\n".join(lines) + "\n")
|
|
||||||
|
|
||||||
|
|
||||||
def install():
|
|
||||||
cupy_pkg = get_cupy_package()
|
|
||||||
if cupy_pkg:
|
|
||||||
update_requirements(cupy_pkg)
|
|
||||||
|
|
||||||
requirements_path = os.path.join(os.path.dirname(__file__), "requirements.txt")
|
|
||||||
subprocess.check_call([
|
|
||||||
sys.executable, "-m", "pip", "install", "-r", requirements_path
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
install()
|
|
||||||
+481
@@ -0,0 +1,481 @@
|
|||||||
|
"""Sequence-native ComfyUI adapter for the official Apache-2.0 LDF-VFI runtime."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import types
|
||||||
|
|
||||||
|
from einops import rearrange, repeat
|
||||||
|
import torch
|
||||||
|
import torch.nn.functional as F
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger("Tween")
|
||||||
|
_LDF_NAMESPACE = "_tween_ldf_upstream"
|
||||||
|
_LDF_IMPORT_LOCK = threading.RLock()
|
||||||
|
|
||||||
|
|
||||||
|
def _cuda_bf16_supported(device: torch.device) -> bool:
|
||||||
|
if device.type != "cuda":
|
||||||
|
return False
|
||||||
|
with torch.cuda.device(device):
|
||||||
|
return torch.cuda.is_bf16_supported()
|
||||||
|
|
||||||
|
|
||||||
|
def _namespace_package(name: str, path: Path):
|
||||||
|
package = sys.modules.get(name)
|
||||||
|
if package is not None:
|
||||||
|
return package
|
||||||
|
package = types.ModuleType(name)
|
||||||
|
package.__path__ = [str(path)]
|
||||||
|
package.__package__ = name
|
||||||
|
sys.modules[name] = package
|
||||||
|
return package
|
||||||
|
|
||||||
|
|
||||||
|
def load_ldf_runtime(source_root: str):
|
||||||
|
"""Load LDF under an isolated namespace without polluting ``training``."""
|
||||||
|
root = Path(source_root).resolve()
|
||||||
|
required = root / "training" / "models" / "precond.py"
|
||||||
|
if not required.is_file():
|
||||||
|
raise RuntimeError(f"Invalid LDF-VFI source directory: missing {required}")
|
||||||
|
|
||||||
|
with _LDF_IMPORT_LOCK:
|
||||||
|
cached = sys.modules.get(f"{_LDF_NAMESPACE}.models.precond")
|
||||||
|
if cached is not None:
|
||||||
|
transformer = importlib.import_module(f"{_LDF_NAMESPACE}.models.transformer_wan")
|
||||||
|
return {
|
||||||
|
"Precond": cached.Precond,
|
||||||
|
"ConditionalVAE": cached.Wan2_1SpatialTiledConditionEncoder3Dv2,
|
||||||
|
"MaskEncoder": cached.MaskSpatialTiledEncoder3D,
|
||||||
|
"Transformer": transformer.WanTransformer3DModel,
|
||||||
|
}
|
||||||
|
|
||||||
|
training_root = _namespace_package(_LDF_NAMESPACE, root / "training")
|
||||||
|
saved_training_modules = {
|
||||||
|
name: module for name, module in tuple(sys.modules.items())
|
||||||
|
if name == "training" or name.startswith("training.")
|
||||||
|
}
|
||||||
|
for name in saved_training_modules:
|
||||||
|
sys.modules.pop(name, None)
|
||||||
|
# One upstream transformer import is absolute (training.distributed.util).
|
||||||
|
# Temporarily alias only while importing, then restore the host process.
|
||||||
|
sys.modules["training"] = training_root
|
||||||
|
try:
|
||||||
|
precond = importlib.import_module(f"{_LDF_NAMESPACE}.models.precond")
|
||||||
|
transformer = importlib.import_module(f"{_LDF_NAMESPACE}.models.transformer_wan")
|
||||||
|
except (ImportError, AttributeError) as exc:
|
||||||
|
for name in tuple(sys.modules):
|
||||||
|
if name == _LDF_NAMESPACE or name.startswith(f"{_LDF_NAMESPACE}."):
|
||||||
|
sys.modules.pop(name, None)
|
||||||
|
raise RuntimeError(
|
||||||
|
"LDF-VFI requires PyTorch 2.5+ and diffusers 0.33+. "
|
||||||
|
"Install Tween's current requirements and restart ComfyUI."
|
||||||
|
) from exc
|
||||||
|
finally:
|
||||||
|
for name in tuple(sys.modules):
|
||||||
|
if name == "training" or name.startswith("training."):
|
||||||
|
sys.modules.pop(name, None)
|
||||||
|
sys.modules.update(saved_training_modules)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"Precond": precond.Precond,
|
||||||
|
"ConditionalVAE": precond.Wan2_1SpatialTiledConditionEncoder3Dv2,
|
||||||
|
"MaskEncoder": precond.MaskSpatialTiledEncoder3D,
|
||||||
|
"Transformer": transformer.WanTransformer3DModel,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _upsample_nearest(frames: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
||||||
|
"""Expand sparse source frames to every temporal position in a window."""
|
||||||
|
kept_indices = torch.where(mask)[0]
|
||||||
|
if kept_indices.numel() == 0:
|
||||||
|
raise RuntimeError("LDF-VFI received a temporal window with no source frames")
|
||||||
|
positions = torch.arange(mask.shape[0], device=mask.device)
|
||||||
|
nearest = (positions[:, None] - kept_indices[None, :]).abs().argmin(dim=1)
|
||||||
|
return frames[nearest]
|
||||||
|
|
||||||
|
|
||||||
|
class LDFVFIModel:
|
||||||
|
"""Long-sequence diffusion interpolation using LDF's skip-concat sampler."""
|
||||||
|
|
||||||
|
TRAIN_FRAMES = 60
|
||||||
|
TILE_TIME = 20
|
||||||
|
CONDITION_TILES = 1
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
model_root: str,
|
||||||
|
vae_path: str,
|
||||||
|
source_root: str,
|
||||||
|
tile_size: int = 256,
|
||||||
|
tile_overlap: int = 64,
|
||||||
|
vae_batch_size: int = 8,
|
||||||
|
attention_type: str = "slide_chunk_all_block_2x1x1",
|
||||||
|
):
|
||||||
|
if tile_size % 8 or tile_overlap % 8:
|
||||||
|
raise ValueError("LDF-VFI tile size and overlap must be divisible by 8")
|
||||||
|
if tile_overlap >= tile_size:
|
||||||
|
raise ValueError("LDF-VFI tile overlap must be smaller than tile size")
|
||||||
|
|
||||||
|
runtime = load_ldf_runtime(source_root)
|
||||||
|
self.device = "cpu"
|
||||||
|
self.dtype = torch.bfloat16
|
||||||
|
self.vae_path = vae_path
|
||||||
|
self._ConditionalVAE = runtime["ConditionalVAE"]
|
||||||
|
self._MaskEncoder = runtime["MaskEncoder"]
|
||||||
|
self._Precond = runtime["Precond"]
|
||||||
|
|
||||||
|
logger.info("Loading LDF-VFI transformer from %s", model_root)
|
||||||
|
try:
|
||||||
|
transformer = runtime["Transformer"].from_pretrained(
|
||||||
|
model_root,
|
||||||
|
subfolder="transformer",
|
||||||
|
torch_dtype=self.dtype,
|
||||||
|
low_cpu_mem_usage=True,
|
||||||
|
)
|
||||||
|
except TypeError:
|
||||||
|
transformer = runtime["Transformer"].from_pretrained(
|
||||||
|
model_root, subfolder="transformer", torch_dtype=self.dtype
|
||||||
|
)
|
||||||
|
transformer.set_attention_type(attention_type)
|
||||||
|
transformer.requires_grad_(False).eval()
|
||||||
|
|
||||||
|
stride = tile_size - tile_overlap
|
||||||
|
tiled_kwargs = {
|
||||||
|
"tile_sample_min_height": tile_size,
|
||||||
|
"tile_sample_min_width": tile_size,
|
||||||
|
"tile_sample_min_time": self.TILE_TIME,
|
||||||
|
"tile_sample_stride_height": stride,
|
||||||
|
"tile_sample_stride_width": stride,
|
||||||
|
"spatial_compression_ratio": 8,
|
||||||
|
"temporal_compression_ratio": 4,
|
||||||
|
}
|
||||||
|
# The same conditional VAE can encode conditions and decode predictions;
|
||||||
|
# sharing it avoids loading a second ~800 MB copy as the reference CLI does.
|
||||||
|
self.vae = self._ConditionalVAE(vae_path, vae_batch_size, **tiled_kwargs)
|
||||||
|
self.mask_encoder = self._MaskEncoder(**tiled_kwargs)
|
||||||
|
self.model = self._Precond(
|
||||||
|
transformer=transformer,
|
||||||
|
vae=self.vae,
|
||||||
|
lq_encoder=self.vae,
|
||||||
|
msk_encoder=self.mask_encoder,
|
||||||
|
)
|
||||||
|
self.model.requires_grad_(False).eval()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def transformer(self):
|
||||||
|
return self.model.transformer
|
||||||
|
|
||||||
|
def _move_auxiliary_models(self, device: torch.device) -> None:
|
||||||
|
self.mask_encoder.mask_encoder.to(device=device, dtype=self.dtype)
|
||||||
|
if self.vae.vae is None:
|
||||||
|
if device.type != "cpu":
|
||||||
|
self.vae.init(device)
|
||||||
|
return
|
||||||
|
self.vae.vae.to(device=device, dtype=self.dtype)
|
||||||
|
if hasattr(self.vae, "mean") and hasattr(self.vae, "std"):
|
||||||
|
self.vae.mean = self.vae.mean.to(device=device, dtype=self.dtype)
|
||||||
|
self.vae.std = self.vae.std.to(device=device, dtype=self.dtype)
|
||||||
|
self.vae.scale = [self.vae.mean, 1.0 / self.vae.std]
|
||||||
|
|
||||||
|
def to(self, device):
|
||||||
|
target = torch.device(device)
|
||||||
|
if target.type == "cuda" and not _cuda_bf16_supported(target):
|
||||||
|
raise RuntimeError("LDF-VFI requires a CUDA GPU with BF16 support (Ampere or newer)")
|
||||||
|
# LDF's custom Wan fork expects its complete condition embedder to use
|
||||||
|
# one dtype: time_embedder feeds time_proj directly without an explicit
|
||||||
|
# cast. Match the official generator's model.to(..., dtype=BF16) call;
|
||||||
|
# preserving diffusers' generic FP32-module policy leaves that pair as
|
||||||
|
# Float/BFloat16 and fails in the first sampling step.
|
||||||
|
self.transformer.to(device=target, dtype=self.dtype)
|
||||||
|
self._move_auxiliary_models(target)
|
||||||
|
self.device = str(target)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def clear_cache(self) -> None:
|
||||||
|
for name in (
|
||||||
|
"_swin_attention_mask",
|
||||||
|
"_sliding_chunk_attention_mask",
|
||||||
|
"_sliding_window_attention_mask",
|
||||||
|
):
|
||||||
|
method = getattr(self.transformer, name, None)
|
||||||
|
cache_clear = getattr(method, "cache_clear", None)
|
||||||
|
if cache_clear is not None:
|
||||||
|
cache_clear()
|
||||||
|
|
||||||
|
def _prepare_condition(self, frames, mask, device):
|
||||||
|
if mask.shape[0] > self.TRAIN_FRAMES:
|
||||||
|
raise ValueError("Internal LDF temporal window exceeds the training window")
|
||||||
|
if mask.shape[0] < self.TRAIN_FRAMES:
|
||||||
|
mask = F.pad(mask, (0, self.TRAIN_FRAMES - mask.shape[0]))
|
||||||
|
dense = _upsample_nearest(frames, mask)
|
||||||
|
dense = rearrange(dense, "t c h w -> 1 c t h w")
|
||||||
|
dense = dense.to(device=device, dtype=self.dtype, non_blocking=True).mul(2).sub(1)
|
||||||
|
dense_mask = repeat(
|
||||||
|
mask, "t -> 1 1 t h w", h=dense.shape[-2], w=dense.shape[-1]
|
||||||
|
).to(device=device, dtype=self.dtype)
|
||||||
|
condition = self.vae.encode(dense, for_train=True)
|
||||||
|
encoded_mask = self.mask_encoder.encode(dense_mask, for_train=True)
|
||||||
|
return dense, dense_mask, condition, encoded_mask
|
||||||
|
|
||||||
|
def _time_schedule(self, num_steps: int, t_shift: float) -> torch.Tensor:
|
||||||
|
schedule = torch.linspace(1.0, 0.0, steps=num_steps + 1)
|
||||||
|
return t_shift * schedule / (1 + (t_shift - 1) * schedule)
|
||||||
|
|
||||||
|
def _predict_step(self, latent, timestep, condition, encoded_mask):
|
||||||
|
return self.model.predict_v(latent, timestep, condition, encoded_mask)
|
||||||
|
|
||||||
|
def _sample_free(self, condition, encoded_mask, schedule, device, progress):
|
||||||
|
latent = torch.randn_like(condition)
|
||||||
|
for index in range(schedule.shape[0] - 1):
|
||||||
|
progress()
|
||||||
|
timestep = torch.full(
|
||||||
|
condition.shape[:-4], float(schedule[index]), device=device, dtype=self.dtype
|
||||||
|
)
|
||||||
|
velocity = self._predict_step(latent, timestep, condition, encoded_mask)
|
||||||
|
step_size = float(schedule[index + 1] - schedule[index])
|
||||||
|
latent = latent + velocity * step_size
|
||||||
|
return latent
|
||||||
|
|
||||||
|
def _sample_between(self, previous, following, condition, encoded_mask,
|
||||||
|
schedule, t_cond, device, progress):
|
||||||
|
previous_noisy = previous * (1 - t_cond) + torch.randn_like(previous) * t_cond
|
||||||
|
following_noisy = following * (1 - t_cond) + torch.randn_like(following) * t_cond
|
||||||
|
middle = torch.randn_like(condition[:, self.CONDITION_TILES:-self.CONDITION_TILES])
|
||||||
|
previous_t = torch.full(
|
||||||
|
previous.shape[:-4], t_cond, device=device, dtype=self.dtype
|
||||||
|
)
|
||||||
|
following_t = torch.full(
|
||||||
|
following.shape[:-4], t_cond, device=device, dtype=self.dtype
|
||||||
|
)
|
||||||
|
for index in range(schedule.shape[0] - 1):
|
||||||
|
progress()
|
||||||
|
latent = torch.cat((previous_noisy, middle, following_noisy), dim=1)
|
||||||
|
middle_t = torch.full(
|
||||||
|
middle.shape[:-4], float(schedule[index]), device=device, dtype=self.dtype
|
||||||
|
)
|
||||||
|
timestep = torch.cat((previous_t, middle_t, following_t), dim=1)
|
||||||
|
velocity = self._predict_step(
|
||||||
|
latent, timestep, condition, encoded_mask
|
||||||
|
)[:, self.CONDITION_TILES:-self.CONDITION_TILES]
|
||||||
|
step_size = float(schedule[index + 1] - schedule[index])
|
||||||
|
middle = middle + velocity * step_size
|
||||||
|
return middle
|
||||||
|
|
||||||
|
def _sample_tail(self, previous, condition, encoded_mask, schedule,
|
||||||
|
t_cond, device, progress):
|
||||||
|
previous_noisy = previous * (1 - t_cond) + torch.randn_like(previous) * t_cond
|
||||||
|
tail = torch.randn_like(condition[:, self.CONDITION_TILES:])
|
||||||
|
previous_t = torch.full(
|
||||||
|
previous.shape[:-4], t_cond, device=device, dtype=self.dtype
|
||||||
|
)
|
||||||
|
for index in range(schedule.shape[0] - 1):
|
||||||
|
progress()
|
||||||
|
latent = torch.cat((previous_noisy, tail), dim=1)
|
||||||
|
tail_t = torch.full(
|
||||||
|
tail.shape[:-4], float(schedule[index]), device=device, dtype=self.dtype
|
||||||
|
)
|
||||||
|
timestep = torch.cat((previous_t, tail_t), dim=1)
|
||||||
|
velocity = self._predict_step(
|
||||||
|
latent, timestep, condition, encoded_mask
|
||||||
|
)[:, self.CONDITION_TILES:]
|
||||||
|
step_size = float(schedule[index + 1] - schedule[index])
|
||||||
|
tail = tail + velocity * step_size
|
||||||
|
return tail
|
||||||
|
|
||||||
|
def _decode(self, latent, dense, dense_mask, height, width):
|
||||||
|
latent = rearrange(
|
||||||
|
latent, "1 nt nh nw c t h w -> 1 nt c t (nh h) (nw w)"
|
||||||
|
)
|
||||||
|
if dense.ndim != 5 or dense_mask.ndim != 5:
|
||||||
|
raise RuntimeError(
|
||||||
|
"LDF-VFI decode conditions must use [batch, channels, time, height, width]"
|
||||||
|
)
|
||||||
|
|
||||||
|
temporal_tiles = latent.shape[1]
|
||||||
|
if dense.shape[2] % temporal_tiles:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"LDF-VFI condition length {dense.shape[2]} is not divisible by "
|
||||||
|
f"the {temporal_tiles} decode tiles"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mirror the official generate.vae_decode adapter. The conditional VAE
|
||||||
|
# consumes one condition and mask tile per latent temporal tile, not a
|
||||||
|
# single continuous 5-D condition tensor.
|
||||||
|
decode_height = latent.shape[-2] * self.vae.spatial_compression_ratio
|
||||||
|
decode_width = latent.shape[-1] * self.vae.spatial_compression_ratio
|
||||||
|
pad_height = decode_height - dense.shape[-2]
|
||||||
|
pad_width = decode_width - dense.shape[-1]
|
||||||
|
if pad_height < 0 or pad_width < 0:
|
||||||
|
raise RuntimeError(
|
||||||
|
"LDF-VFI decoded latent is smaller than its conditioning frames; "
|
||||||
|
"check the VAE tile and overlap settings"
|
||||||
|
)
|
||||||
|
dense = F.pad(dense, (0, pad_width, 0, pad_height))
|
||||||
|
dense = rearrange(
|
||||||
|
dense, "b c (nt t) h w -> b nt c t h w", nt=temporal_tiles
|
||||||
|
)
|
||||||
|
|
||||||
|
dense_mask = dense_mask[..., 0, 0]
|
||||||
|
dense_mask = repeat(
|
||||||
|
dense_mask, "b c t -> b c t h w", h=decode_height, w=decode_width
|
||||||
|
)
|
||||||
|
dense_mask = rearrange(
|
||||||
|
dense_mask, "b c (nt t) h w -> b nt c t h w", nt=temporal_tiles
|
||||||
|
)
|
||||||
|
|
||||||
|
prediction = self.vae.decode(
|
||||||
|
latent, dense, dense_mask
|
||||||
|
)[..., :height, :width]
|
||||||
|
return rearrange(prediction, "1 c t h w -> t c h w").add(1).mul(0.5).clamp_(0, 1).float().cpu()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sampling_block_count(num_input_frames: int, temporal_factor: int) -> int:
|
||||||
|
total_length = num_input_frames * temporal_factor
|
||||||
|
t0 = 40
|
||||||
|
stride = 20
|
||||||
|
blocks = 1
|
||||||
|
while t0 + stride <= total_length - 1:
|
||||||
|
blocks += 2
|
||||||
|
t0 += stride * 2
|
||||||
|
if t0 < total_length:
|
||||||
|
blocks += 1
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
@torch.no_grad()
|
||||||
|
def interpolate_sequence(
|
||||||
|
self,
|
||||||
|
frames: torch.Tensor,
|
||||||
|
temporal_factor: int,
|
||||||
|
num_steps: int = 16,
|
||||||
|
t_shift: float = 8.0,
|
||||||
|
t_cond: float = 0.1,
|
||||||
|
seed: int = 42,
|
||||||
|
progress_callback=None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
if not 2 <= temporal_factor <= 16:
|
||||||
|
raise ValueError("LDF-VFI temporal factor must be between 2 and 16")
|
||||||
|
if num_steps < 1:
|
||||||
|
raise ValueError("LDF-VFI num_steps must be at least 1")
|
||||||
|
if t_shift <= 0:
|
||||||
|
raise ValueError("LDF-VFI t_shift must be greater than 0")
|
||||||
|
if not 0 <= t_cond <= 1:
|
||||||
|
raise ValueError("LDF-VFI t_cond must be between 0 and 1")
|
||||||
|
if frames.shape[0] < 2:
|
||||||
|
return frames
|
||||||
|
|
||||||
|
device = next(self.transformer.parameters()).device
|
||||||
|
if device.type != "cuda":
|
||||||
|
raise RuntimeError("Move LDF-VFI to a CUDA device before interpolation")
|
||||||
|
source = frames.detach().float().cpu()
|
||||||
|
height, width = source.shape[-2:]
|
||||||
|
schedule = self._time_schedule(num_steps, t_shift)
|
||||||
|
progress_callback = progress_callback or (lambda: None)
|
||||||
|
|
||||||
|
cuda_index = device.index
|
||||||
|
if cuda_index is None:
|
||||||
|
cuda_index = torch.cuda.current_device()
|
||||||
|
rng_context = torch.random.fork_rng(devices=[cuda_index])
|
||||||
|
with rng_context:
|
||||||
|
torch.manual_seed(int(seed))
|
||||||
|
with torch.cuda.device(device):
|
||||||
|
torch.cuda.manual_seed(int(seed))
|
||||||
|
chunks = self._interpolate_sequence_impl(
|
||||||
|
source, temporal_factor, schedule, t_cond, device, progress_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
result = torch.cat(chunks, dim=0)
|
||||||
|
expected = (frames.shape[0] - 1) * temporal_factor + 1
|
||||||
|
return result[:expected]
|
||||||
|
|
||||||
|
def _interpolate_sequence_impl(self, source, factor, schedule, t_cond,
|
||||||
|
device, progress):
|
||||||
|
total_mask = torch.zeros(source.shape[0] * factor, dtype=torch.bool)
|
||||||
|
total_mask[::factor] = True
|
||||||
|
n_tiles = self.TRAIN_FRAMES // self.TILE_TIME
|
||||||
|
output_tiles = n_tiles - self.CONDITION_TILES
|
||||||
|
outputs = []
|
||||||
|
|
||||||
|
# First chunk.
|
||||||
|
mask = total_mask[:self.TRAIN_FRAMES]
|
||||||
|
input_end = int(mask.sum())
|
||||||
|
dense, dense_mask, condition, encoded_mask = self._prepare_condition(
|
||||||
|
source[:input_end], mask, device
|
||||||
|
)
|
||||||
|
latent = self._sample_free(condition, encoded_mask, schedule, device, progress)
|
||||||
|
latent = latent[:, :output_tiles]
|
||||||
|
previous = latent[:, -self.CONDITION_TILES:]
|
||||||
|
decode_time = output_tiles * self.TILE_TIME
|
||||||
|
outputs.append(self._decode(
|
||||||
|
latent, dense[:, :, :decode_time], dense_mask[:, :, :decode_time],
|
||||||
|
source.shape[-2], source.shape[-1],
|
||||||
|
))
|
||||||
|
|
||||||
|
t0 = output_tiles * self.TILE_TIME
|
||||||
|
stride = (n_tiles - self.CONDITION_TILES * 2) * self.TILE_TIME
|
||||||
|
while t0 + stride <= total_mask.shape[0] - 1:
|
||||||
|
# A future/skip chunk establishes the far-side condition.
|
||||||
|
input_start = t0 + stride - self.CONDITION_TILES * self.TILE_TIME
|
||||||
|
input_end_t = t0 + stride * 2 + self.CONDITION_TILES * self.TILE_TIME
|
||||||
|
mask = total_mask[input_start:input_end_t]
|
||||||
|
source_start = int(total_mask[:input_start].sum())
|
||||||
|
source_end = int(total_mask[:input_end_t].sum())
|
||||||
|
dense, dense_mask, condition, encoded_mask = self._prepare_condition(
|
||||||
|
source[source_start:source_end], mask, device
|
||||||
|
)
|
||||||
|
skip = self._sample_free(condition, encoded_mask, schedule, device, progress)
|
||||||
|
skip = skip[:, self.CONDITION_TILES:-self.CONDITION_TILES]
|
||||||
|
following = skip[:, :self.CONDITION_TILES]
|
||||||
|
previous_next = skip[:, -self.CONDITION_TILES:]
|
||||||
|
start_time = self.CONDITION_TILES * self.TILE_TIME
|
||||||
|
end_time = output_tiles * self.TILE_TIME
|
||||||
|
decoded_skip = self._decode(
|
||||||
|
skip, dense[:, :, start_time:end_time], dense_mask[:, :, start_time:end_time],
|
||||||
|
source.shape[-2], source.shape[-1],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fill the gap between the preceding and skip chunks.
|
||||||
|
input_start = t0 - self.CONDITION_TILES * self.TILE_TIME
|
||||||
|
input_end_t = t0 + stride + self.CONDITION_TILES * self.TILE_TIME
|
||||||
|
mask = total_mask[input_start:input_end_t]
|
||||||
|
source_start = int(total_mask[:input_start].sum())
|
||||||
|
source_end = int(total_mask[:input_end_t].sum())
|
||||||
|
dense, dense_mask, condition, encoded_mask = self._prepare_condition(
|
||||||
|
source[source_start:source_end], mask, device
|
||||||
|
)
|
||||||
|
middle = self._sample_between(
|
||||||
|
previous, following, condition, encoded_mask,
|
||||||
|
schedule, t_cond, device, progress,
|
||||||
|
)
|
||||||
|
decoded_middle = self._decode(
|
||||||
|
middle, dense[:, :, start_time:end_time], dense_mask[:, :, start_time:end_time],
|
||||||
|
source.shape[-2], source.shape[-1],
|
||||||
|
)
|
||||||
|
outputs.extend((decoded_middle, decoded_skip))
|
||||||
|
previous = previous_next
|
||||||
|
t0 += stride * 2
|
||||||
|
|
||||||
|
# Remaining tail.
|
||||||
|
if t0 < total_mask.shape[0]:
|
||||||
|
input_start = t0 - self.CONDITION_TILES * self.TILE_TIME
|
||||||
|
mask = total_mask[input_start:]
|
||||||
|
source_start = int(total_mask[:input_start].sum())
|
||||||
|
dense, dense_mask, condition, encoded_mask = self._prepare_condition(
|
||||||
|
source[source_start:], mask, device
|
||||||
|
)
|
||||||
|
tail = self._sample_tail(
|
||||||
|
previous, condition, encoded_mask, schedule,
|
||||||
|
t_cond, device, progress,
|
||||||
|
)
|
||||||
|
start_time = self.CONDITION_TILES * self.TILE_TIME
|
||||||
|
outputs.append(self._decode(
|
||||||
|
tail, dense[:, :, start_time:], dense_mask[:, :, start_time:],
|
||||||
|
source.shape[-2], source.shape[-1],
|
||||||
|
))
|
||||||
|
|
||||||
|
return outputs
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
[project]
|
||||||
|
name = "comfyui-tween"
|
||||||
|
description = "Video frame interpolation nodes for ComfyUI using BIM-VFI, EMA-VFI, SGM-VFI, GIMM-VFI, SPEED, and LDF-VFI."
|
||||||
|
version = "1.2.2"
|
||||||
|
license = "Apache-2.0"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"gdown",
|
||||||
|
"timm",
|
||||||
|
"omegaconf",
|
||||||
|
"yacs",
|
||||||
|
"easydict",
|
||||||
|
"einops",
|
||||||
|
"huggingface_hub",
|
||||||
|
"diffusers>=0.33.1,<0.40",
|
||||||
|
"accelerate>=1.5,<2",
|
||||||
|
"safetensors",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Repository = "https://github.com/Ethanfel/ComfyUI-Tween"
|
||||||
|
|
||||||
|
[tool.comfy]
|
||||||
|
PublisherId = "ethanfel"
|
||||||
|
DisplayName = "Tween - Video Frame Interpolation"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
addopts = "--confcutdir=tests"
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
gdown
|
gdown
|
||||||
|
timm
|
||||||
omegaconf
|
omegaconf
|
||||||
yacs
|
yacs
|
||||||
easydict
|
easydict
|
||||||
einops
|
einops
|
||||||
huggingface_hub
|
huggingface_hub
|
||||||
|
diffusers>=0.33.1,<0.40
|
||||||
|
accelerate>=1.5,<2
|
||||||
|
safetensors
|
||||||
|
|||||||
+87
-10
@@ -1,7 +1,13 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
|
try:
|
||||||
import cupy
|
import cupy
|
||||||
|
except Exception:
|
||||||
|
# Broad catch: an installed-but-broken cupy (e.g. incompatible NumPy)
|
||||||
|
# raises non-ImportError exceptions at import time. Treat any failure as
|
||||||
|
# "cupy unavailable" and fall back to the pure-PyTorch implementation.
|
||||||
|
cupy = None
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import torch
|
import torch
|
||||||
@@ -216,20 +222,91 @@ def cuda_kernel(strFunction:str, strKernel:str, objVariables:typing.Dict):
|
|||||||
# end
|
# end
|
||||||
|
|
||||||
|
|
||||||
@cupy.memoize(for_each_device=True)
|
_cuda_launch_cache = {}
|
||||||
def cuda_launch(strKey:str):
|
|
||||||
if 'CUDA_HOME' not in os.environ:
|
|
||||||
os.environ['CUDA_HOME'] = cupy.cuda.get_cuda_path()
|
|
||||||
# end
|
|
||||||
|
|
||||||
return cupy.RawKernel(objCudacache[strKey]['strKernel'], objCudacache[strKey]['strFunction'],
|
def cuda_launch(strKey:str):
|
||||||
options=tuple(['-I ' + os.environ['CUDA_HOME'], '-I ' + os.environ['CUDA_HOME'] + '/include']))
|
if strKey not in _cuda_launch_cache:
|
||||||
|
if 'CUDA_HOME' not in os.environ:
|
||||||
|
try:
|
||||||
|
cuda_path = cupy.cuda.get_cuda_path()
|
||||||
|
except Exception:
|
||||||
|
cuda_path = None
|
||||||
|
if cuda_path is None:
|
||||||
|
cuda_path = '/usr/local/cuda'
|
||||||
|
os.environ['CUDA_HOME'] = cuda_path
|
||||||
|
_cuda_launch_cache[strKey] = cupy.RawKernel(
|
||||||
|
objCudacache[strKey]['strKernel'],
|
||||||
|
objCudacache[strKey]['strFunction'],
|
||||||
|
options=tuple(['-I ' + os.environ['CUDA_HOME'],
|
||||||
|
'-I ' + os.environ['CUDA_HOME'] + '/include'])
|
||||||
|
)
|
||||||
|
return _cuda_launch_cache[strKey]
|
||||||
# end
|
# end
|
||||||
|
|
||||||
|
|
||||||
##########################################################
|
##########################################################
|
||||||
|
|
||||||
|
|
||||||
|
def _pytorch_softsplat_impl(tenIn, tenFlow):
|
||||||
|
"""Pure-PyTorch forward warp via bilinear splatting (scatter_add)."""
|
||||||
|
B, C, H, W = tenIn.shape
|
||||||
|
tenOut = tenIn.new_zeros(B, C, H, W)
|
||||||
|
|
||||||
|
grid_y, grid_x = torch.meshgrid(
|
||||||
|
torch.arange(H, device=tenIn.device, dtype=tenIn.dtype),
|
||||||
|
torch.arange(W, device=tenIn.device, dtype=tenIn.dtype),
|
||||||
|
indexing='ij',
|
||||||
|
)
|
||||||
|
|
||||||
|
flt_x = grid_x.unsqueeze(0) + tenFlow[:, 0, :, :]
|
||||||
|
flt_y = grid_y.unsqueeze(0) + tenFlow[:, 1, :, :]
|
||||||
|
|
||||||
|
valid = torch.isfinite(flt_x) & torch.isfinite(flt_y)
|
||||||
|
flt_x = torch.where(valid, flt_x, torch.zeros_like(flt_x))
|
||||||
|
flt_y = torch.where(valid, flt_y, torch.zeros_like(flt_y))
|
||||||
|
|
||||||
|
nw_x = flt_x.floor().long()
|
||||||
|
nw_y = flt_y.floor().long()
|
||||||
|
frac_x = flt_x - nw_x.to(flt_x.dtype)
|
||||||
|
frac_y = flt_y - nw_y.to(flt_y.dtype)
|
||||||
|
|
||||||
|
w_nw = (1.0 - frac_x) * (1.0 - frac_y) * valid
|
||||||
|
w_ne = frac_x * (1.0 - frac_y) * valid
|
||||||
|
w_sw = (1.0 - frac_x) * frac_y * valid
|
||||||
|
w_se = frac_x * frac_y * valid
|
||||||
|
|
||||||
|
out_flat = tenOut.view(B, C, -1)
|
||||||
|
|
||||||
|
for dx, dy, w in [(0, 0, w_nw), (1, 0, w_ne), (0, 1, w_sw), (1, 1, w_se)]:
|
||||||
|
tx = nw_x + dx
|
||||||
|
ty = nw_y + dy
|
||||||
|
in_bounds = (tx >= 0) & (tx < W) & (ty >= 0) & (ty < H)
|
||||||
|
w_masked = w * in_bounds
|
||||||
|
idx = (ty.clamp(0, H - 1) * W + tx.clamp(0, W - 1))
|
||||||
|
idx = idx.unsqueeze(1).expand_as(tenIn)
|
||||||
|
weighted = tenIn * w_masked.unsqueeze(1)
|
||||||
|
out_flat.scatter_add_(2, idx.reshape(B, C, -1), weighted.reshape(B, C, -1))
|
||||||
|
|
||||||
|
return tenOut
|
||||||
|
|
||||||
|
|
||||||
|
_softsplat_fn = None
|
||||||
|
|
||||||
|
def _pytorch_softsplat(tenIn, tenFlow):
|
||||||
|
global _softsplat_fn
|
||||||
|
if _softsplat_fn is None:
|
||||||
|
try:
|
||||||
|
_softsplat_fn = torch.compile(_pytorch_softsplat_impl)
|
||||||
|
except Exception:
|
||||||
|
_softsplat_fn = _pytorch_softsplat_impl
|
||||||
|
try:
|
||||||
|
return _softsplat_fn(tenIn, tenFlow)
|
||||||
|
except Exception:
|
||||||
|
_softsplat_fn = _pytorch_softsplat_impl
|
||||||
|
return _softsplat_fn(tenIn, tenFlow)
|
||||||
|
# end
|
||||||
|
|
||||||
|
|
||||||
def softsplat(tenIn:torch.Tensor, tenFlow:torch.Tensor, tenMetric:torch.Tensor, strMode:str):
|
def softsplat(tenIn:torch.Tensor, tenFlow:torch.Tensor, tenMetric:torch.Tensor, strMode:str):
|
||||||
assert(strMode.split('-')[0] in ['sum', 'avg', 'linear', 'soft'])
|
assert(strMode.split('-')[0] in ['sum', 'avg', 'linear', 'soft'])
|
||||||
|
|
||||||
@@ -281,7 +358,7 @@ class softsplat_func(torch.autograd.Function):
|
|||||||
def forward(self, tenIn, tenFlow):
|
def forward(self, tenIn, tenFlow):
|
||||||
tenOut = tenIn.new_zeros([tenIn.shape[0], tenIn.shape[1], tenIn.shape[2], tenIn.shape[3]])
|
tenOut = tenIn.new_zeros([tenIn.shape[0], tenIn.shape[1], tenIn.shape[2], tenIn.shape[3]])
|
||||||
|
|
||||||
if tenIn.is_cuda == True:
|
if tenIn.is_cuda and cupy is not None:
|
||||||
cuda_launch(cuda_kernel('softsplat_out', '''
|
cuda_launch(cuda_kernel('softsplat_out', '''
|
||||||
extern "C" __global__ void __launch_bounds__(512) softsplat_out(
|
extern "C" __global__ void __launch_bounds__(512) softsplat_out(
|
||||||
const int n,
|
const int n,
|
||||||
@@ -345,8 +422,8 @@ class softsplat_func(torch.autograd.Function):
|
|||||||
stream=collections.namedtuple('Stream', 'ptr')(torch.cuda.current_stream().cuda_stream)
|
stream=collections.namedtuple('Stream', 'ptr')(torch.cuda.current_stream().cuda_stream)
|
||||||
)
|
)
|
||||||
|
|
||||||
elif tenIn.is_cuda != True:
|
else:
|
||||||
assert(False)
|
tenOut = _pytorch_softsplat(tenIn, tenFlow)
|
||||||
|
|
||||||
# end
|
# end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
"""ComfyUI inference adapter for the official SPEED runtime."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from contextlib import nullcontext
|
||||||
|
import importlib
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger("Tween")
|
||||||
|
_SPEED_NAMESPACE = "_tween_speed_upstream"
|
||||||
|
|
||||||
|
|
||||||
|
def _cuda_bf16_supported(device: torch.device) -> bool:
|
||||||
|
if device.type != "cuda":
|
||||||
|
return False
|
||||||
|
with torch.cuda.device(device):
|
||||||
|
return torch.cuda.is_bf16_supported()
|
||||||
|
|
||||||
|
|
||||||
|
def _namespace_package(name: str, path: Path) -> None:
|
||||||
|
if name in sys.modules:
|
||||||
|
return
|
||||||
|
package = types.ModuleType(name)
|
||||||
|
package.__path__ = [str(path)]
|
||||||
|
package.__package__ = name
|
||||||
|
sys.modules[name] = package
|
||||||
|
|
||||||
|
|
||||||
|
def load_speed_model_class(source_root: str):
|
||||||
|
"""Import SpeedDiT without adding the upstream repository to sys.path."""
|
||||||
|
root = Path(source_root).resolve()
|
||||||
|
model_file = root / "src" / "models" / "model.py"
|
||||||
|
if not model_file.is_file():
|
||||||
|
raise RuntimeError(f"Invalid SPEED source directory: missing {model_file}")
|
||||||
|
|
||||||
|
_namespace_package(_SPEED_NAMESPACE, root)
|
||||||
|
_namespace_package(f"{_SPEED_NAMESPACE}.src", root / "src")
|
||||||
|
_namespace_package(f"{_SPEED_NAMESPACE}.src.models", root / "src" / "models")
|
||||||
|
module = importlib.import_module(f"{_SPEED_NAMESPACE}.src.models.model")
|
||||||
|
return module.SpeedDiT
|
||||||
|
|
||||||
|
|
||||||
|
class SpeedVFIModel:
|
||||||
|
"""Midpoint interpolation wrapper around the official SPEED SpeedDiT."""
|
||||||
|
|
||||||
|
def __init__(self, checkpoint_path: str, source_root: str,
|
||||||
|
precision: str = "auto", device: str = "cpu"):
|
||||||
|
SpeedDiT = load_speed_model_class(source_root)
|
||||||
|
self.model = SpeedDiT(
|
||||||
|
hidden_dim=768,
|
||||||
|
head_dim=64,
|
||||||
|
depths=(2, 6, 4),
|
||||||
|
patch_sizes=(64, 32, 16),
|
||||||
|
)
|
||||||
|
self.precision = precision
|
||||||
|
self.device = str(device)
|
||||||
|
self._seed = 0
|
||||||
|
self._generator = None
|
||||||
|
self._generator_device = None
|
||||||
|
self._load_checkpoint(checkpoint_path)
|
||||||
|
self.model.requires_grad_(False).eval()
|
||||||
|
self.to(device)
|
||||||
|
|
||||||
|
def _load_checkpoint(self, checkpoint_path: str) -> None:
|
||||||
|
checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
|
||||||
|
state_dict = checkpoint.get("model", checkpoint) if isinstance(checkpoint, dict) else checkpoint
|
||||||
|
if not isinstance(state_dict, dict):
|
||||||
|
raise TypeError(f"SPEED checkpoint does not contain a state dict: {checkpoint_path}")
|
||||||
|
if state_dict and all(key.startswith("module.") for key in state_dict):
|
||||||
|
state_dict = {key[len("module."):]: value for key, value in state_dict.items()}
|
||||||
|
self.model.load_state_dict(state_dict, strict=True)
|
||||||
|
|
||||||
|
def _autocast_dtype(self, device: torch.device):
|
||||||
|
if self.precision == "fp32" or device.type != "cuda":
|
||||||
|
return None
|
||||||
|
if self.precision == "fp16":
|
||||||
|
return torch.float16
|
||||||
|
if self.precision == "bf16":
|
||||||
|
if not _cuda_bf16_supported(device):
|
||||||
|
raise RuntimeError(
|
||||||
|
"SPEED BF16 precision requires a CUDA GPU with BF16 support; "
|
||||||
|
"select auto or fp16 on this GPU"
|
||||||
|
)
|
||||||
|
return torch.bfloat16
|
||||||
|
if _cuda_bf16_supported(device):
|
||||||
|
return torch.bfloat16
|
||||||
|
return torch.float16
|
||||||
|
|
||||||
|
def to(self, device):
|
||||||
|
target = torch.device(device)
|
||||||
|
self.device = str(target)
|
||||||
|
# Match the official runtime: retain FP32 weights and use autocast for
|
||||||
|
# CUDA inference. This also avoids mixed-dtype timestep embedding bugs.
|
||||||
|
self.model.to(device=target, dtype=torch.float32)
|
||||||
|
# Keep the generator alive across CPU offloading. Recreating it on every
|
||||||
|
# pair batch would restart the noise stream whenever keep_device=False.
|
||||||
|
# _get_generator replaces it automatically if inference changes device.
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_seed(self, seed: int) -> None:
|
||||||
|
self._seed = int(seed)
|
||||||
|
self._generator = None
|
||||||
|
self._generator_device = None
|
||||||
|
|
||||||
|
def reset_seed(self) -> None:
|
||||||
|
self.set_seed(self._seed)
|
||||||
|
|
||||||
|
def clear_cache(self) -> None:
|
||||||
|
rope = getattr(self.model, "rope_embedder", None)
|
||||||
|
cache = getattr(rope, "rope_cache", None)
|
||||||
|
if cache is not None:
|
||||||
|
cache.clear()
|
||||||
|
|
||||||
|
def _get_generator(self, device: torch.device) -> torch.Generator:
|
||||||
|
device_name = str(device)
|
||||||
|
if self._generator is None or self._generator_device != device_name:
|
||||||
|
self._generator = torch.Generator(device=device)
|
||||||
|
self._generator.manual_seed(self._seed)
|
||||||
|
self._generator_device = device_name
|
||||||
|
return self._generator
|
||||||
|
|
||||||
|
@torch.no_grad()
|
||||||
|
def interpolate_batch(self, frames0, frames1, time_step=0.5):
|
||||||
|
if abs(float(time_step) - 0.5) > 1e-6:
|
||||||
|
raise ValueError("SPEED's released checkpoint supports midpoint interpolation only")
|
||||||
|
|
||||||
|
device = next(self.model.parameters()).device
|
||||||
|
frame0 = frames0.to(device=device, dtype=torch.float32, non_blocking=True).mul(2).sub(1)
|
||||||
|
frame1 = frames1.to(device=device, dtype=torch.float32, non_blocking=True).mul(2).sub(1)
|
||||||
|
cond_frames = torch.cat((frame0, frame1), dim=0)
|
||||||
|
noisy_frames = torch.randn(
|
||||||
|
frame0.shape,
|
||||||
|
generator=self._get_generator(device),
|
||||||
|
device=device,
|
||||||
|
dtype=torch.float32,
|
||||||
|
)
|
||||||
|
timestep = torch.full(
|
||||||
|
(frame0.shape[0],), 1000.0, device=device, dtype=torch.float32
|
||||||
|
)
|
||||||
|
autocast_dtype = self._autocast_dtype(device)
|
||||||
|
autocast = (
|
||||||
|
torch.autocast(device_type="cuda", dtype=autocast_dtype)
|
||||||
|
if autocast_dtype is not None
|
||||||
|
else nullcontext()
|
||||||
|
)
|
||||||
|
with autocast:
|
||||||
|
prediction = self.model(
|
||||||
|
noisy_frames=noisy_frames,
|
||||||
|
cond_frames=cond_frames,
|
||||||
|
timestep=timestep,
|
||||||
|
)
|
||||||
|
return prediction.div(2).add(0.5).clamp_(0, 1).float()
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import importlib
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
PACKAGE_NAME = "_tween_bim_tests"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_inference_module():
|
||||||
|
package = types.ModuleType(PACKAGE_NAME)
|
||||||
|
package.__path__ = [str(REPO_ROOT)]
|
||||||
|
package.__package__ = PACKAGE_NAME
|
||||||
|
sys.modules.setdefault(PACKAGE_NAME, package)
|
||||||
|
return importlib.import_module(f"{PACKAGE_NAME}.inference")
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_pyramid_levels_match_official_video_inference():
|
||||||
|
model_class = _load_inference_module().BiMVFIModel
|
||||||
|
model = model_class.__new__(model_class)
|
||||||
|
model.auto_pyr_level = True
|
||||||
|
|
||||||
|
assert model._get_pyr_level(240) == 5
|
||||||
|
assert model._get_pyr_level(539) == 5
|
||||||
|
assert model._get_pyr_level(720) == 5
|
||||||
|
assert model._get_pyr_level(1079) == 5
|
||||||
|
assert model._get_pyr_level(1080) == 6
|
||||||
|
assert model._get_pyr_level(2159) == 6
|
||||||
|
assert model._get_pyr_level(2160) == 7
|
||||||
|
|
||||||
|
|
||||||
|
def test_manual_pyramid_level_remains_available():
|
||||||
|
model_class = _load_inference_module().BiMVFIModel
|
||||||
|
model = model_class.__new__(model_class)
|
||||||
|
model.auto_pyr_level = False
|
||||||
|
model.pyr_level = 3
|
||||||
|
|
||||||
|
assert model._get_pyr_level(720) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_artifact_safe_mode_removes_only_rgb_refinement_residual():
|
||||||
|
module = _load_inference_module()
|
||||||
|
official = module.BiMVFI(pyr_level=3, feat_channels=1)
|
||||||
|
artifact_safe = module.BiMVFI(
|
||||||
|
pyr_level=3,
|
||||||
|
feat_channels=1,
|
||||||
|
artifact_safe_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert official.sn.use_rgb_refine_residual is True
|
||||||
|
assert artifact_safe.sn.use_rgb_refine_residual is False
|
||||||
|
|
||||||
|
official_keys = official.state_dict().keys()
|
||||||
|
artifact_safe_keys = artifact_safe.state_dict().keys()
|
||||||
|
assert official_keys == artifact_safe_keys
|
||||||
|
|
||||||
|
warped0 = torch.full((1, 3, 2, 2), 0.2)
|
||||||
|
warped1 = torch.full((1, 3, 2, 2), 0.8)
|
||||||
|
mask = torch.full((1, 1, 2, 2), 0.25)
|
||||||
|
residual = torch.full((1, 3, 2, 2), 0.1)
|
||||||
|
blend = warped0 * mask + warped1 * (1 - mask)
|
||||||
|
|
||||||
|
assert torch.equal(
|
||||||
|
official.sn.merge_warped_images(warped0, warped1, mask, residual),
|
||||||
|
blend + residual,
|
||||||
|
)
|
||||||
|
assert torch.equal(
|
||||||
|
artifact_safe.sn.merge_warped_images(warped0, warped1, mask, residual),
|
||||||
|
blend,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_demo_exposes_artifact_safe_mode_without_enabling_it():
|
||||||
|
import json
|
||||||
|
|
||||||
|
workflow_path = REPO_ROOT / "example_workflows" / "tween_speed_bim_model_lab.json"
|
||||||
|
workflow = json.loads(workflow_path.read_text(encoding="utf-8"))
|
||||||
|
loader = next(node for node in workflow["nodes"] if node["type"] == "LoadBIMVFIModel")
|
||||||
|
|
||||||
|
assert loader["widgets_values"] == ["bim_vfi.pth", True, 3, False]
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
WORKFLOW_PATH = (
|
||||||
|
Path(__file__).resolve().parents[1]
|
||||||
|
/ "example_workflows"
|
||||||
|
/ "tween_speed_bim_model_lab.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _workflow():
|
||||||
|
return json.loads(WORKFLOW_PATH.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_speed_bim_demo_is_clean_and_current():
|
||||||
|
workflow = _workflow()
|
||||||
|
node_types = [node["type"] for node in workflow["nodes"]]
|
||||||
|
|
||||||
|
assert "Note" not in node_types
|
||||||
|
assert not any("LDF" in node_type for node_type in node_types)
|
||||||
|
assert node_types.count("SPEEDVFIInterpolate") == 1
|
||||||
|
assert node_types.count("BIMVFIInterpolate") == 1
|
||||||
|
assert [group["title"] for group in workflow["groups"]] == [
|
||||||
|
"INPUT · 24 FPS / 25 FRAMES",
|
||||||
|
"SPEED",
|
||||||
|
"BIM-VFI",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_speed_bim_demo_links_are_internally_consistent():
|
||||||
|
workflow = _workflow()
|
||||||
|
nodes = {node["id"]: node for node in workflow["nodes"]}
|
||||||
|
node_ids = set(nodes)
|
||||||
|
links = {link[0]: link for link in workflow["links"]}
|
||||||
|
|
||||||
|
assert workflow["last_node_id"] == max(node_ids)
|
||||||
|
assert workflow["last_link_id"] == max(links)
|
||||||
|
|
||||||
|
for link_id, source_id, source_slot, target_id, target_slot, _ in workflow["links"]:
|
||||||
|
assert link_id in links
|
||||||
|
assert source_id in node_ids
|
||||||
|
assert target_id in node_ids
|
||||||
|
assert link_id in nodes[source_id]["outputs"][source_slot]["links"]
|
||||||
|
assert nodes[target_id]["inputs"][target_slot]["link"] == link_id
|
||||||
|
|
||||||
|
for node in workflow["nodes"]:
|
||||||
|
for input_slot in node.get("inputs", []):
|
||||||
|
if input_slot.get("link") is not None:
|
||||||
|
assert input_slot["link"] in links
|
||||||
|
for output_slot in node.get("outputs", []):
|
||||||
|
for link_id in output_slot.get("links") or []:
|
||||||
|
assert link_id in links
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import tomllib
|
||||||
|
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def _requirements():
|
||||||
|
return [
|
||||||
|
line.strip()
|
||||||
|
for line in (REPO_ROOT / "requirements.txt").read_text().splitlines()
|
||||||
|
if line.strip() and not line.lstrip().startswith("#")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_comfy_manager_has_no_redundant_install_script():
|
||||||
|
assert not (REPO_ROOT / "install.py").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_declared_dependencies_stay_aligned_and_exclude_optional_cupy():
|
||||||
|
with (REPO_ROOT / "pyproject.toml").open("rb") as file:
|
||||||
|
project_dependencies = tomllib.load(file)["project"]["dependencies"]
|
||||||
|
|
||||||
|
requirements = _requirements()
|
||||||
|
assert requirements == project_dependencies
|
||||||
|
assert not any("cupy" in dependency.lower() for dependency in requirements)
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import torch
|
||||||
|
import pytest
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from ldf_backend import LDFVFIModel
|
||||||
|
|
||||||
|
|
||||||
|
class _RecordingConditionalVAE:
|
||||||
|
spatial_compression_ratio = 8
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.decode_shapes = None
|
||||||
|
|
||||||
|
def decode(self, latent, condition, mask):
|
||||||
|
self.decode_shapes = (
|
||||||
|
tuple(latent.shape),
|
||||||
|
tuple(condition.shape),
|
||||||
|
tuple(mask.shape),
|
||||||
|
)
|
||||||
|
# A small real result lets _decode finish without materializing the
|
||||||
|
# full 720p tensors used for the shape-only inputs above.
|
||||||
|
return torch.zeros(1, 3, 40, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
|
class _RecordingTransformer:
|
||||||
|
def __init__(self):
|
||||||
|
self.to_kwargs = None
|
||||||
|
|
||||||
|
def to(self, **kwargs):
|
||||||
|
self.to_kwargs = kwargs
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class _ShapeOnlyLDF(LDFVFIModel):
|
||||||
|
"""Exercise sequence chunking without loading the multi-GB checkpoint."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.decode_tile_counts = []
|
||||||
|
|
||||||
|
def _prepare_condition(self, frames, mask, device):
|
||||||
|
assert frames.shape[0] == int(mask.sum())
|
||||||
|
dense = torch.empty(1, 3, self.TRAIN_FRAMES, 1, 1)
|
||||||
|
dense_mask = torch.empty(1, 1, self.TRAIN_FRAMES, 1, 1)
|
||||||
|
latent = torch.empty(
|
||||||
|
1, self.TRAIN_FRAMES // self.TILE_TIME, 1, 1, 1, 1, 1, 1
|
||||||
|
)
|
||||||
|
return dense, dense_mask, latent, latent
|
||||||
|
|
||||||
|
def _sample_free(self, condition, encoded_mask, schedule, device, progress):
|
||||||
|
return condition
|
||||||
|
|
||||||
|
def _sample_between(
|
||||||
|
self, previous, following, condition, encoded_mask,
|
||||||
|
schedule, t_cond, device, progress,
|
||||||
|
):
|
||||||
|
return condition[:, self.CONDITION_TILES:-self.CONDITION_TILES]
|
||||||
|
|
||||||
|
def _sample_tail(
|
||||||
|
self, previous, condition, encoded_mask, schedule,
|
||||||
|
t_cond, device, progress,
|
||||||
|
):
|
||||||
|
return condition[:, self.CONDITION_TILES:]
|
||||||
|
|
||||||
|
def _decode(self, latent, dense, dense_mask, height, width):
|
||||||
|
temporal_tiles = latent.shape[1]
|
||||||
|
assert dense.shape[2] == temporal_tiles * self.TILE_TIME
|
||||||
|
assert dense_mask.shape[2] == dense.shape[2]
|
||||||
|
self.decode_tile_counts.append(temporal_tiles)
|
||||||
|
return torch.empty(dense.shape[2], 3, height, width)
|
||||||
|
|
||||||
|
|
||||||
|
def test_decode_tiles_40_frame_720p_condition_for_conditional_vae():
|
||||||
|
"""Regression for the 5-D condition passed to upstream VAE.decode."""
|
||||||
|
model = LDFVFIModel.__new__(LDFVFIModel)
|
||||||
|
model.vae = _RecordingConditionalVAE()
|
||||||
|
|
||||||
|
# Representative first LDF output block at 720p. Meta tensors exercise
|
||||||
|
# exact shape transforms without allocating hundreds of MB in the test.
|
||||||
|
latent = torch.empty((1, 2, 4, 7, 4, 5, 24, 24), device="meta")
|
||||||
|
condition = torch.empty((1, 3, 40, 720, 1280), device="meta")
|
||||||
|
mask = torch.empty((1, 1, 40, 720, 1280), device="meta")
|
||||||
|
|
||||||
|
result = model._decode(latent, condition, mask, height=720, width=1280)
|
||||||
|
|
||||||
|
assert model.vae.decode_shapes == (
|
||||||
|
(1, 2, 4, 5, 96, 168),
|
||||||
|
(1, 2, 3, 20, 768, 1344),
|
||||||
|
(1, 2, 1, 20, 768, 1344),
|
||||||
|
)
|
||||||
|
assert result.shape == (40, 3, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_device_move_casts_complete_ldf_transformer_to_bfloat16():
|
||||||
|
model = LDFVFIModel.__new__(LDFVFIModel)
|
||||||
|
transformer = _RecordingTransformer()
|
||||||
|
model.model = SimpleNamespace(transformer=transformer)
|
||||||
|
model.dtype = torch.bfloat16
|
||||||
|
model._move_auxiliary_models = lambda device: None
|
||||||
|
|
||||||
|
model.to("cpu")
|
||||||
|
|
||||||
|
assert transformer.to_kwargs == {
|
||||||
|
"device": torch.device("cpu"),
|
||||||
|
"dtype": torch.bfloat16,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_decode_rejects_condition_length_that_cannot_tile():
|
||||||
|
model = LDFVFIModel.__new__(LDFVFIModel)
|
||||||
|
model.vae = _RecordingConditionalVAE()
|
||||||
|
latent = torch.empty((1, 2, 1, 1, 4, 5, 1, 1), device="meta")
|
||||||
|
condition = torch.empty((1, 3, 39, 8, 8), device="meta")
|
||||||
|
mask = torch.empty((1, 1, 39, 8, 8), device="meta")
|
||||||
|
|
||||||
|
try:
|
||||||
|
model._decode(latent, condition, mask, height=8, width=8)
|
||||||
|
except RuntimeError as error:
|
||||||
|
assert "not divisible" in str(error)
|
||||||
|
else:
|
||||||
|
raise AssertionError("Expected an invalid temporal tile error")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("temporal_factor", [2, 3, 8, 16])
|
||||||
|
def test_sequence_chunk_conditions_align_for_reported_391_frames(temporal_factor):
|
||||||
|
model = _ShapeOnlyLDF()
|
||||||
|
source = torch.empty(391, 3, 1, 1)
|
||||||
|
chunks = model._interpolate_sequence_impl(
|
||||||
|
source=source,
|
||||||
|
factor=temporal_factor,
|
||||||
|
schedule=torch.tensor([1.0, 0.0]),
|
||||||
|
t_cond=0.1,
|
||||||
|
device=torch.device("cpu"),
|
||||||
|
progress=lambda: None,
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_frames = (source.shape[0] - 1) * temporal_factor + 1
|
||||||
|
assert sum(chunk.shape[0] for chunk in chunks) >= expected_frames
|
||||||
|
assert len(chunks) == model.sampling_block_count(
|
||||||
|
source.shape[0], temporal_factor
|
||||||
|
)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import ast
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
NODE_SOURCE = Path(__file__).resolve().parents[1] / "nodes.py"
|
||||||
|
TIMED_CLASSES = {
|
||||||
|
"BIMVFIInterpolate",
|
||||||
|
"BIMVFISegmentInterpolate",
|
||||||
|
"EMAVFIInterpolate",
|
||||||
|
"EMAVFISegmentInterpolate",
|
||||||
|
"SGMVFIInterpolate",
|
||||||
|
"SGMVFISegmentInterpolate",
|
||||||
|
"LDFVFIInterpolate",
|
||||||
|
"SPEEDVFIInterpolate",
|
||||||
|
"SPEEDVFISegmentInterpolate",
|
||||||
|
"GIMMVFIInterpolate",
|
||||||
|
"GIMMVFISegmentInterpolate",
|
||||||
|
}
|
||||||
|
DIRECTLY_DECORATED = TIMED_CLASSES - {
|
||||||
|
"SPEEDVFIInterpolate",
|
||||||
|
"SPEEDVFISegmentInterpolate",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _class_definitions():
|
||||||
|
tree = ast.parse(NODE_SOURCE.read_text(encoding="utf-8"))
|
||||||
|
return {
|
||||||
|
node.name: node
|
||||||
|
for node in tree.body
|
||||||
|
if isinstance(node, ast.ClassDef) and node.name in TIMED_CLASSES
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _literal_assignment(class_node, name):
|
||||||
|
for statement in class_node.body:
|
||||||
|
if (
|
||||||
|
isinstance(statement, ast.Assign)
|
||||||
|
and len(statement.targets) == 1
|
||||||
|
and isinstance(statement.targets[0], ast.Name)
|
||||||
|
and statement.targets[0].id == name
|
||||||
|
):
|
||||||
|
return ast.literal_eval(statement.value)
|
||||||
|
raise AssertionError(f"{class_node.name} does not define {name}")
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_interpolation_nodes_expose_elapsed_seconds_last():
|
||||||
|
classes = _class_definitions()
|
||||||
|
assert classes.keys() == TIMED_CLASSES
|
||||||
|
|
||||||
|
for class_node in classes.values():
|
||||||
|
assert _literal_assignment(class_node, "RETURN_TYPES")[-1] == "FLOAT"
|
||||||
|
assert _literal_assignment(class_node, "RETURN_NAMES")[-1] == "elapsed_seconds"
|
||||||
|
|
||||||
|
|
||||||
|
def test_direct_interpolation_methods_append_timing_output():
|
||||||
|
classes = _class_definitions()
|
||||||
|
for class_name in DIRECTLY_DECORATED:
|
||||||
|
interpolate = next(
|
||||||
|
statement
|
||||||
|
for statement in classes[class_name].body
|
||||||
|
if isinstance(statement, ast.FunctionDef)
|
||||||
|
and statement.name == "interpolate"
|
||||||
|
)
|
||||||
|
assert any(
|
||||||
|
isinstance(decorator, ast.Call)
|
||||||
|
and isinstance(decorator.func, ast.Name)
|
||||||
|
and decorator.func.id == "_with_elapsed_seconds"
|
||||||
|
for decorator in interpolate.decorator_list
|
||||||
|
)
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { app } from "../../scripts/app.js";
|
|
||||||
import { api } from "../../scripts/api.js";
|
|
||||||
|
|
||||||
function fitHeight(node) {
|
|
||||||
node.setSize([node.size[0], node.computeSize([node.size[0], node.size[1]])[1]]);
|
|
||||||
node?.graph?.setDirtyCanvas(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
app.registerExtension({
|
|
||||||
name: "Tween.VideoPreview",
|
|
||||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
|
||||||
if (nodeData?.name !== "TweenConcatVideos") return;
|
|
||||||
|
|
||||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
|
||||||
nodeType.prototype.onNodeCreated = function () {
|
|
||||||
onNodeCreated?.apply(this, arguments);
|
|
||||||
|
|
||||||
const container = document.createElement("div");
|
|
||||||
const previewWidget = this.addDOMWidget("videopreview", "preview", container, {
|
|
||||||
serialize: false,
|
|
||||||
hideOnZoom: false,
|
|
||||||
getValue() { return container.value; },
|
|
||||||
setValue(v) { container.value = v; },
|
|
||||||
});
|
|
||||||
|
|
||||||
previewWidget.computeSize = function (width) {
|
|
||||||
if (this.aspectRatio && !this.videoEl.hidden) {
|
|
||||||
const height = (previewNode.size[0] - 20) / this.aspectRatio + 10;
|
|
||||||
return [width, height > 0 ? height : -4];
|
|
||||||
}
|
|
||||||
return [width, -4];
|
|
||||||
};
|
|
||||||
|
|
||||||
const previewNode = this;
|
|
||||||
|
|
||||||
previewWidget.videoEl = document.createElement("video");
|
|
||||||
previewWidget.videoEl.controls = true;
|
|
||||||
previewWidget.videoEl.loop = true;
|
|
||||||
previewWidget.videoEl.muted = true;
|
|
||||||
previewWidget.videoEl.style.width = "100%";
|
|
||||||
previewWidget.videoEl.hidden = true;
|
|
||||||
|
|
||||||
previewWidget.videoEl.addEventListener("loadedmetadata", () => {
|
|
||||||
previewWidget.aspectRatio = previewWidget.videoEl.videoWidth / previewWidget.videoEl.videoHeight;
|
|
||||||
fitHeight(previewNode);
|
|
||||||
});
|
|
||||||
previewWidget.videoEl.addEventListener("error", () => {
|
|
||||||
previewWidget.videoEl.hidden = true;
|
|
||||||
fitHeight(previewNode);
|
|
||||||
});
|
|
||||||
|
|
||||||
container.appendChild(previewWidget.videoEl);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onExecuted = nodeType.prototype.onExecuted;
|
|
||||||
nodeType.prototype.onExecuted = function (message) {
|
|
||||||
onExecuted?.apply(this, arguments);
|
|
||||||
|
|
||||||
if (!message?.gifs?.length) return;
|
|
||||||
|
|
||||||
const params = message.gifs[0];
|
|
||||||
const previewWidget = this.widgets?.find((w) => w.name === "videopreview");
|
|
||||||
if (!previewWidget) return;
|
|
||||||
|
|
||||||
const query = new URLSearchParams(params);
|
|
||||||
query.set("timestamp", Date.now());
|
|
||||||
previewWidget.videoEl.src = api.apiURL("/view?" + query);
|
|
||||||
previewWidget.videoEl.hidden = false;
|
|
||||||
previewWidget.videoEl.autoplay = true;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user