Automated PR - 2024-12-19
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
# ComfyUI-LTXVideo
|
||||
ComfyUI-LTXVideo is a collection of custom nodes for ComfyUI designed to integrate the LTXVideo diffusion model. These nodes enable workflows for text-to-video, image-to-video, and video-to-video generation. The main LTXVideo repository can be found [here](https://github.com/Lightricks/LTX-Video).
|
||||
|
||||
## 19.12.2024 ⭐ Update ⭐
|
||||
1. Improved model - removes "strobing texture" artifacts and generates better motion. Download from [here](https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltx-video-2b-v0.9.1.safetensors).
|
||||
2. STG support
|
||||
3. Integrated image degradation system for improved motion generation.
|
||||
3. Additional initial latent optional input to chain latents for high res generation.
|
||||
4. Image captioning in image to video [flow](assets/ltxvideo-i2v.json).
|
||||
|
||||
## Installation
|
||||
|
||||
Installation via [ComfyUI-Manager](https://github.com/ltdrdata/ComfyUI-Manager) is preferred. Simply search for `ComfyUI-LTXVideo` in the list of nodes and follow installation instructions.
|
||||
@@ -20,27 +27,19 @@ For portable ComfyUI installations, run
|
||||
|
||||
### Models
|
||||
|
||||
1. Download [ltx-video-2b-v0.9.safetensors](https://huggingface.co/Lightricks/LTX-Video/blob/main/ltx-video-2b-v0.9.safetensors) from Hugging Face and place it under `models/checkpoints`.
|
||||
2. [Install git-lfs](https://docs.github.com/en/repositories/working-with-files/managing-large-files/installing-git-large-file-storage) and clone the text encoder model to `models/text_encoders`:
|
||||
```bash
|
||||
cd models/text_encoders && git clone https://huggingface.co/PixArt-alpha/PixArt-XL-2-1024-MS
|
||||
```
|
||||
1. Download [ltx-video-2b-v0.9.1.safetensors](https://huggingface.co/Lightricks/LTX-Video/blob/main/ltx-video-2b-v0.9.1.safetensors) from Hugging Face and place it under `models/checkpoints`.
|
||||
2. Install one of the t5 text encoders, for example [google_t5-v1_1-xxl_encoderonly](https://huggingface.co/mcmonkey/google_t5-v1_1-xxl_encoderonly/tree/main). You can install it using ComfyUI Model Manager.
|
||||
|
||||
## Example workflows
|
||||
|
||||
Note that to run the example workflows, you need to have [ComfyUI-VideoHelperSuite](https://github.com/kosinkadink/ComfyUI-VideoHelperSuite) installed.
|
||||
|
||||
### Text-to-video
|
||||
|
||||
[Download workflow](assets/ltxvideo-t2v.json)
|
||||

|
||||
Note that to run the example workflows, you need to have some additional custom nodes, like [ComfyUI-VideoHelperSuite](https://github.com/kosinkadink/ComfyUI-VideoHelperSuite) and others, installed. You can do it by pressing "Install Missing Custom Nodes" button in ComfyUI Manager.
|
||||
|
||||
### Image-to-video
|
||||
|
||||
[Download workflow](assets/ltxvideo-i2v.json)
|
||||

|
||||
|
||||
### Video-to-video
|
||||
### Text-to-video
|
||||
|
||||
[Download workflow](assets/ltxvideo-v2v.json)
|
||||

|
||||
[Download workflow](assets/ltxvideo-t2v.json)
|
||||

|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .loader_node import LTXVLoader # noqa: F401
|
||||
from .nodes_registry import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
from .stg import LTXVApplySTG, STGGuiderNode # noqa: F401
|
||||
from .t5_encoder import LTXVCLIPModelLoader # noqa: F401
|
||||
from .transformer import LTXVModelConfigurator, LTXVShiftSigmas # noqa: F401
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:57f8f573779ebb37ca2c2bd1377e430d4e9abce0e728cf72228687b72c455244
|
||||
size 699352
|
||||
+1235
-455
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f247a5e042f13afcf0e6a88ea94e4187b6eb079d904ef6f54b7aeda47710c7ac
|
||||
size 711224
|
||||
oid sha256:6ec16f1372ece0cd63395edf29d9997599580df3703d7fd30025fd0870ad5532
|
||||
size 1205996
|
||||
|
||||
+995
-455
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:156f19cbe09b2510a4c86803e7dc07e510643f2ff6d709bfd821cf0cceb59e8c
|
||||
size 494899
|
||||
oid sha256:1e508763410d009f524ed22c1c420e798904afe92098130a4d155d8318ef6b3a
|
||||
size 1112804
|
||||
|
||||
@@ -1,914 +0,0 @@
|
||||
{
|
||||
"last_node_id": 110,
|
||||
"last_link_id": 271,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 105,
|
||||
"type": "LTXVCLIPModelLoader",
|
||||
"pos": [
|
||||
-458,
|
||||
-122
|
||||
],
|
||||
"size": [
|
||||
314.85308837890625,
|
||||
60.962398529052734
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"links": [
|
||||
264,
|
||||
265
|
||||
],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LTXVCLIPModelLoader"
|
||||
},
|
||||
"widgets_values": [
|
||||
"PixArt-XL-2-1024-MS/text_encoder/model-00001-of-00002.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 87,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [
|
||||
-27,
|
||||
-245
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
200
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 264
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [
|
||||
215
|
||||
],
|
||||
"slot_index": 0,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"A group of colorful hot air balloons take off at dawn in Cappadocia, Turkey.\nDozens of balloons in various bright colors and patterns slowly rise into the pink and orange sky. Below them, the unique landscape of Cappadocia unfolds, with its distinctive \"fairy chimneys\" - tall, cone-shaped rock formations scattered across the valley. The rising sun casts long shadows across the terrain, highlighting the otherworldly topography."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"type": "SamplerCustomAdvanced",
|
||||
"pos": [
|
||||
1107,
|
||||
-259
|
||||
],
|
||||
"size": [
|
||||
355.20001220703125,
|
||||
106
|
||||
],
|
||||
"flags": {},
|
||||
"order": 12,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "noise",
|
||||
"type": "NOISE",
|
||||
"link": 72
|
||||
},
|
||||
{
|
||||
"name": "guider",
|
||||
"type": "GUIDER",
|
||||
"link": 87,
|
||||
"slot_index": 1
|
||||
},
|
||||
{
|
||||
"name": "sampler",
|
||||
"type": "SAMPLER",
|
||||
"link": 70
|
||||
},
|
||||
{
|
||||
"name": "sigmas",
|
||||
"type": "SIGMAS",
|
||||
"link": 270
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": 258
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "output",
|
||||
"type": "LATENT",
|
||||
"links": [
|
||||
78
|
||||
],
|
||||
"slot_index": 0,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "denoised_output",
|
||||
"type": "LATENT",
|
||||
"links": [],
|
||||
"slot_index": 1,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SamplerCustomAdvanced"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"type": "KSamplerSelect",
|
||||
"pos": [
|
||||
768,
|
||||
-132
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
58
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "SAMPLER",
|
||||
"type": "SAMPLER",
|
||||
"links": [
|
||||
70
|
||||
],
|
||||
"slot_index": 0,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerSelect"
|
||||
},
|
||||
"widgets_values": [
|
||||
"euler"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 81,
|
||||
"type": "BasicScheduler",
|
||||
"pos": [
|
||||
409,
|
||||
-10
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
106
|
||||
],
|
||||
"flags": {},
|
||||
"order": 9,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": 261
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "SIGMAS",
|
||||
"type": "SIGMAS",
|
||||
"links": [
|
||||
262
|
||||
],
|
||||
"slot_index": 0,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "BasicScheduler"
|
||||
},
|
||||
"widgets_values": [
|
||||
"normal",
|
||||
20,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
1491,
|
||||
-252
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 13,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": 78
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 256,
|
||||
"slot_index": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
121
|
||||
],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 103,
|
||||
"type": "LTXVModelConfigurator",
|
||||
"pos": [
|
||||
-22,
|
||||
257
|
||||
],
|
||||
"size": [
|
||||
378,
|
||||
242
|
||||
],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": 254
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 255
|
||||
},
|
||||
{
|
||||
"name": "conditioning",
|
||||
"type": "IMAGE",
|
||||
"link": 271,
|
||||
"shape": 7
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"links": [
|
||||
260,
|
||||
261
|
||||
],
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "latent",
|
||||
"type": "LATENT",
|
||||
"links": [
|
||||
258
|
||||
],
|
||||
"slot_index": 1
|
||||
},
|
||||
{
|
||||
"name": "sigma_shift",
|
||||
"type": "FLOAT",
|
||||
"links": [
|
||||
259
|
||||
],
|
||||
"slot_index": 2
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LTXVModelConfigurator"
|
||||
},
|
||||
"widgets_values": [
|
||||
"Custom",
|
||||
768,
|
||||
512,
|
||||
65,
|
||||
25,
|
||||
1,
|
||||
true
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 60,
|
||||
"type": "VHS_VideoCombine",
|
||||
"pos": [
|
||||
1197,
|
||||
-104
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
520.6666870117188
|
||||
],
|
||||
"flags": {},
|
||||
"order": 14,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 121
|
||||
},
|
||||
{
|
||||
"name": "audio",
|
||||
"type": "AUDIO",
|
||||
"link": null,
|
||||
"shape": 7
|
||||
},
|
||||
{
|
||||
"name": "meta_batch",
|
||||
"type": "VHS_BatchManager",
|
||||
"link": null,
|
||||
"shape": 7
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": null,
|
||||
"shape": 7
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "Filenames",
|
||||
"type": "VHS_FILENAMES",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VHS_VideoCombine"
|
||||
},
|
||||
"widgets_values": {
|
||||
"frame_rate": 25,
|
||||
"loop_count": 0,
|
||||
"filename_prefix": "LTXVideo",
|
||||
"format": "video/h264-mp4",
|
||||
"pix_fmt": "yuv420p",
|
||||
"crf": 19,
|
||||
"save_metadata": true,
|
||||
"pingpong": false,
|
||||
"save_output": true,
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false,
|
||||
"params": {
|
||||
"filename": "LTXVideo_00002.mp4",
|
||||
"subfolder": "",
|
||||
"type": "output",
|
||||
"format": "video/h264-mp4",
|
||||
"frame_rate": 25
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 88,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [
|
||||
-29,
|
||||
5
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
200
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 265
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [
|
||||
214
|
||||
],
|
||||
"slot_index": 0,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"worst quality, inconsistent motion, blurry, jittery, distorted, watermarks"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 104,
|
||||
"type": "LTXVShiftSigmas",
|
||||
"pos": [
|
||||
759,
|
||||
-19
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
106
|
||||
],
|
||||
"flags": {},
|
||||
"order": 10,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "sigmas",
|
||||
"type": "SIGMAS",
|
||||
"link": 262
|
||||
},
|
||||
{
|
||||
"name": "sigma_shift",
|
||||
"type": "FLOAT",
|
||||
"link": 259,
|
||||
"widget": {
|
||||
"name": "sigma_shift"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "SIGMAS",
|
||||
"type": "SIGMAS",
|
||||
"links": [
|
||||
269
|
||||
],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LTXVShiftSigmas"
|
||||
},
|
||||
"widgets_values": [
|
||||
1.820833333,
|
||||
true,
|
||||
0.1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 109,
|
||||
"type": "SplitSigmas",
|
||||
"pos": [
|
||||
761,
|
||||
137
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
78
|
||||
],
|
||||
"flags": {},
|
||||
"order": 11,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "sigmas",
|
||||
"type": "SIGMAS",
|
||||
"link": 269
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "high_sigmas",
|
||||
"type": "SIGMAS",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "low_sigmas",
|
||||
"type": "SIGMAS",
|
||||
"links": [
|
||||
270
|
||||
],
|
||||
"slot_index": 1
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SplitSigmas"
|
||||
},
|
||||
"widgets_values": [
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 42,
|
||||
"type": "CFGGuider",
|
||||
"pos": [
|
||||
766,
|
||||
265
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
98
|
||||
],
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": 260
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": 215
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": 214
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "GUIDER",
|
||||
"type": "GUIDER",
|
||||
"links": [
|
||||
87
|
||||
],
|
||||
"slot_index": 0,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CFGGuider"
|
||||
},
|
||||
"widgets_values": [
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"type": "RandomNoise",
|
||||
"pos": [
|
||||
765,
|
||||
-259
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
82
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "NOISE",
|
||||
"type": "NOISE",
|
||||
"links": [
|
||||
72
|
||||
],
|
||||
"slot_index": 0,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "RandomNoise"
|
||||
},
|
||||
"widgets_values": [
|
||||
43,
|
||||
"fixed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 110,
|
||||
"type": "VHS_LoadVideo",
|
||||
"pos": [
|
||||
-442,
|
||||
-10
|
||||
],
|
||||
"size": [
|
||||
283.6130065917969,
|
||||
473.7097473144531
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "meta_batch",
|
||||
"type": "VHS_BatchManager",
|
||||
"link": null,
|
||||
"shape": 7
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": null,
|
||||
"shape": 7
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
271
|
||||
],
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "frame_count",
|
||||
"type": "INT",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "audio",
|
||||
"type": "AUDIO",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "video_info",
|
||||
"type": "VHS_VIDEOINFO",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VHS_LoadVideo"
|
||||
},
|
||||
"widgets_values": {
|
||||
"video": "video.mp4",
|
||||
"force_rate": 0,
|
||||
"force_size": "Disabled",
|
||||
"custom_width": 512,
|
||||
"custom_height": 512,
|
||||
"frame_load_cap": 0,
|
||||
"skip_first_frames": 0,
|
||||
"select_every_nth": 1,
|
||||
"choose video to upload": "image",
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false,
|
||||
"params": {
|
||||
"force_rate": 0,
|
||||
"frame_load_cap": 0,
|
||||
"skip_first_frames": 0,
|
||||
"select_every_nth": 1,
|
||||
"filename": "video.mp4",
|
||||
"type": "input",
|
||||
"format": "video/mp4"
|
||||
},
|
||||
"muted": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 102,
|
||||
"type": "LTXVLoader",
|
||||
"pos": [
|
||||
-461,
|
||||
-249
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
78
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"links": [
|
||||
254
|
||||
],
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"links": [
|
||||
255,
|
||||
256
|
||||
],
|
||||
"slot_index": 1
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LTXVLoader"
|
||||
},
|
||||
"widgets_values": [
|
||||
"ltx-video-2b-v0.9.safetensors"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
70,
|
||||
14,
|
||||
0,
|
||||
36,
|
||||
2,
|
||||
"SAMPLER"
|
||||
],
|
||||
[
|
||||
72,
|
||||
37,
|
||||
0,
|
||||
36,
|
||||
0,
|
||||
"NOISE"
|
||||
],
|
||||
[
|
||||
78,
|
||||
36,
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
"LATENT"
|
||||
],
|
||||
[
|
||||
87,
|
||||
42,
|
||||
0,
|
||||
36,
|
||||
1,
|
||||
"GUIDER"
|
||||
],
|
||||
[
|
||||
121,
|
||||
8,
|
||||
0,
|
||||
60,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
214,
|
||||
88,
|
||||
0,
|
||||
42,
|
||||
2,
|
||||
"CONDITIONING"
|
||||
],
|
||||
[
|
||||
215,
|
||||
87,
|
||||
0,
|
||||
42,
|
||||
1,
|
||||
"CONDITIONING"
|
||||
],
|
||||
[
|
||||
254,
|
||||
102,
|
||||
0,
|
||||
103,
|
||||
0,
|
||||
"MODEL"
|
||||
],
|
||||
[
|
||||
255,
|
||||
102,
|
||||
1,
|
||||
103,
|
||||
1,
|
||||
"VAE"
|
||||
],
|
||||
[
|
||||
256,
|
||||
102,
|
||||
1,
|
||||
8,
|
||||
1,
|
||||
"VAE"
|
||||
],
|
||||
[
|
||||
258,
|
||||
103,
|
||||
1,
|
||||
36,
|
||||
4,
|
||||
"LATENT"
|
||||
],
|
||||
[
|
||||
259,
|
||||
103,
|
||||
2,
|
||||
104,
|
||||
1,
|
||||
"FLOAT"
|
||||
],
|
||||
[
|
||||
260,
|
||||
103,
|
||||
0,
|
||||
42,
|
||||
0,
|
||||
"MODEL"
|
||||
],
|
||||
[
|
||||
261,
|
||||
103,
|
||||
0,
|
||||
81,
|
||||
0,
|
||||
"MODEL"
|
||||
],
|
||||
[
|
||||
262,
|
||||
81,
|
||||
0,
|
||||
104,
|
||||
0,
|
||||
"SIGMAS"
|
||||
],
|
||||
[
|
||||
264,
|
||||
105,
|
||||
0,
|
||||
87,
|
||||
0,
|
||||
"CLIP"
|
||||
],
|
||||
[
|
||||
265,
|
||||
105,
|
||||
0,
|
||||
88,
|
||||
0,
|
||||
"CLIP"
|
||||
],
|
||||
[
|
||||
269,
|
||||
104,
|
||||
0,
|
||||
109,
|
||||
0,
|
||||
"SIGMAS"
|
||||
],
|
||||
[
|
||||
270,
|
||||
109,
|
||||
1,
|
||||
36,
|
||||
3,
|
||||
"SIGMAS"
|
||||
],
|
||||
[
|
||||
271,
|
||||
110,
|
||||
0,
|
||||
103,
|
||||
2,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.6115909044841471,
|
||||
"offset": [
|
||||
595.203978662415,
|
||||
513.6446771351451
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:22d3a1732d18e5aaf2c3fd8263e271cdd40b6e0384eaef1082eac6cc39c0cd60
|
||||
size 708563
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8681ff86edb43d0a12472175d5503a590910b24d5ec5d5d5b78eed079960eac0
|
||||
size 998115
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:610240226d1f9f7e89e6d585176d952787d79523403d1c9a6c2feaba85e8ed35
|
||||
size 1538809
|
||||
+66
-7
@@ -1,3 +1,6 @@
|
||||
import io
|
||||
|
||||
import av
|
||||
import comfy.latent_formats
|
||||
import comfy.model_base
|
||||
import comfy.model_management
|
||||
@@ -5,10 +8,52 @@ import comfy.model_patcher
|
||||
import comfy.sd
|
||||
import comfy.supported_models_base
|
||||
import comfy.utils
|
||||
import numpy as np
|
||||
import torch
|
||||
from ltx_video.models.autoencoders.vae_encode import get_vae_size_scale_factor
|
||||
|
||||
|
||||
def encode_single_frame(output_file, image_array: np.ndarray, crf):
|
||||
container = av.open(output_file, "w", format="mp4")
|
||||
try:
|
||||
stream = container.add_stream(
|
||||
"h264", rate=1, options={"crf": str(crf), "preset": "veryfast"}
|
||||
)
|
||||
stream.height = image_array.shape[0]
|
||||
stream.width = image_array.shape[1]
|
||||
av_frame = av.VideoFrame.from_ndarray(image_array, format="rgb24").reformat(
|
||||
format="yuv420p"
|
||||
)
|
||||
container.mux(stream.encode(av_frame))
|
||||
container.mux(stream.encode())
|
||||
finally:
|
||||
container.close()
|
||||
|
||||
|
||||
def decode_single_frame(video_file):
|
||||
container = av.open(video_file)
|
||||
try:
|
||||
stream = next(s for s in container.streams if s.type == "video")
|
||||
frame = next(container.decode(stream))
|
||||
finally:
|
||||
container.close()
|
||||
return frame.to_ndarray(format="rgb24")
|
||||
|
||||
|
||||
def videofy(image: torch.Tensor, crf=29):
|
||||
if crf == 0:
|
||||
return image
|
||||
|
||||
image_array = (image * 255.0).byte().cpu().numpy()
|
||||
with io.BytesIO() as output_file:
|
||||
encode_single_frame(output_file, image_array, crf)
|
||||
video_bytes = output_file.getvalue()
|
||||
with io.BytesIO(video_bytes) as video_file:
|
||||
image_array = decode_single_frame(video_file)
|
||||
tensor = torch.tensor(image_array, dtype=image.dtype, device=image.device) / 255.0
|
||||
return tensor
|
||||
|
||||
|
||||
def pad_tensor(tensor, target_len):
|
||||
dim = 2
|
||||
repeat_factor = target_len - tensor.shape[dim] # Ceiling division
|
||||
@@ -17,24 +62,38 @@ def pad_tensor(tensor, target_len):
|
||||
return torch.cat([tensor, padding], dim=dim)
|
||||
|
||||
|
||||
def encode_media_conditioning(init_media, vae, width, height, frames_number):
|
||||
def encode_media_conditioning(
|
||||
init_media, vae, width, height, frames_number, image_compression, initial_latent
|
||||
):
|
||||
pixels = comfy.utils.common_upscale(
|
||||
init_media.movedim(-1, 1), width, height, "bilinear", "center"
|
||||
init_media.movedim(-1, 1), width, height, "bilinear", ""
|
||||
).movedim(1, -1)
|
||||
encode_pixels = pixels[:, :, :, :3]
|
||||
init_latents = vae.encode(encode_pixels).float()
|
||||
if image_compression > 0:
|
||||
for i in range(encode_pixels.shape[0]):
|
||||
image = videofy(encode_pixels[i], image_compression)
|
||||
encode_pixels[i] = image
|
||||
|
||||
encoded_latents = vae.encode(encode_pixels).float()
|
||||
|
||||
video_scale_factor, _, _ = get_vae_size_scale_factor(vae.first_stage_model)
|
||||
video_scale_factor = video_scale_factor if frames_number > 1 else 1
|
||||
target_len = (frames_number // video_scale_factor) + 1
|
||||
init_latents = init_latents[:, :, :target_len]
|
||||
encoded_latents = encoded_latents[:, :, :target_len]
|
||||
|
||||
if initial_latent is None:
|
||||
initial_latent = encoded_latents
|
||||
else:
|
||||
if encoded_latents.shape[2] > initial_latent.shape[2]:
|
||||
initial_latent = pad_tensor(initial_latent, encoded_latents.shape[2])
|
||||
initial_latent[:, :, : encoded_latents.shape[2], ...] = encoded_latents
|
||||
|
||||
init_image_frame_number = init_media.shape[0]
|
||||
if init_image_frame_number == 1:
|
||||
result = pad_tensor(init_latents, target_len)
|
||||
result = pad_tensor(initial_latent, target_len)
|
||||
elif init_image_frame_number % 8 != 1:
|
||||
result = pad_tensor(init_latents, target_len)
|
||||
result = pad_tensor(initial_latent, target_len)
|
||||
else:
|
||||
result = init_latents
|
||||
result = initial_latent
|
||||
|
||||
return result
|
||||
|
||||
+67
-44
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import comfy
|
||||
@@ -11,6 +12,7 @@ from ltx_video.models.autoencoders.causal_video_autoencoder import (
|
||||
)
|
||||
from ltx_video.models.transformers.symmetric_patchifier import SymmetricPatchifier
|
||||
from ltx_video.models.transformers.transformer3d import Transformer3DModel
|
||||
from safetensors import safe_open
|
||||
|
||||
from .model import LTXVModel, LTXVModelConfig, LTXVTransformer3D
|
||||
from .nodes_registry import comfy_node
|
||||
@@ -27,7 +29,7 @@ class LTXVLoader:
|
||||
folder_paths.get_filename_list("checkpoints"),
|
||||
{"tooltip": "The name of the checkpoint (model) to load."},
|
||||
),
|
||||
"dtype": (["bfloat16", "float32"], {"default": "float32"}),
|
||||
"dtype": (["bfloat16", "float32"], {"default": "bfloat16"}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,9 +47,20 @@ class LTXVLoader:
|
||||
|
||||
ckpt_path = Path(folder_paths.get_full_path("checkpoints", ckpt_name))
|
||||
|
||||
weights = safetensors.torch.load_file(ckpt_path, device=str(load_device))
|
||||
vae_config = None
|
||||
unet_config = None
|
||||
with safe_open(ckpt_path, framework="pt", device="cpu") as f:
|
||||
metadata = f.metadata()
|
||||
if metadata is not None:
|
||||
config_metadata = metadata.get("config", None)
|
||||
if config_metadata is not None:
|
||||
config_metadata = json.loads(config_metadata)
|
||||
vae_config = config_metadata.get("vae", None)
|
||||
unet_config = config_metadata.get("transformer", None)
|
||||
|
||||
vae = self._load_vae(weights)
|
||||
weights = safetensors.torch.load_file(ckpt_path, device="cpu")
|
||||
|
||||
vae = self._load_vae(weights, vae_config)
|
||||
num_latent_channels = vae.first_stage_model.config.latent_channels
|
||||
|
||||
model = self._load_unet(
|
||||
@@ -56,14 +69,13 @@ class LTXVLoader:
|
||||
weights,
|
||||
num_latent_channels,
|
||||
dtype=dtype_map[dtype],
|
||||
config=unet_config,
|
||||
)
|
||||
return (model, vae)
|
||||
|
||||
def _load_vae(self, weights):
|
||||
vae_prefix = "vae."
|
||||
vae = LTXVVAE.from_config_and_state_dict(
|
||||
vae_class=CausalVideoAutoencoder,
|
||||
config={
|
||||
def _load_vae(self, weights, config=None):
|
||||
if config is None:
|
||||
config = {
|
||||
"_class_name": "CausalVideoAutoencoder",
|
||||
"dims": 3,
|
||||
"in_channels": 3,
|
||||
@@ -87,7 +99,11 @@ class LTXVLoader:
|
||||
"latent_log_var": "uniform",
|
||||
"use_quant_conv": False,
|
||||
"causal_decoder": False,
|
||||
},
|
||||
}
|
||||
vae_prefix = "vae."
|
||||
vae = LTXVVAE.from_config_and_state_dict(
|
||||
vae_class=CausalVideoAutoencoder,
|
||||
config=config,
|
||||
state_dict={
|
||||
key.removeprefix(vae_prefix): value
|
||||
for key, value in weights.items()
|
||||
@@ -97,40 +113,47 @@ class LTXVLoader:
|
||||
return vae
|
||||
|
||||
def _load_unet(
|
||||
self, load_device, offload_device, weights, num_latent_channels, dtype
|
||||
self,
|
||||
load_device,
|
||||
offload_device,
|
||||
weights,
|
||||
num_latent_channels,
|
||||
dtype,
|
||||
config=None,
|
||||
):
|
||||
config = {
|
||||
"_class_name": "Transformer3DModel",
|
||||
"_diffusers_version": "0.25.1",
|
||||
"_name_or_path": "PixArt-alpha/PixArt-XL-2-256x256",
|
||||
"activation_fn": "gelu-approximate",
|
||||
"attention_bias": True,
|
||||
"attention_head_dim": 64,
|
||||
"attention_type": "default",
|
||||
"caption_channels": 4096,
|
||||
"cross_attention_dim": 2048,
|
||||
"double_self_attention": False,
|
||||
"dropout": 0.0,
|
||||
"in_channels": 128,
|
||||
"norm_elementwise_affine": False,
|
||||
"norm_eps": 1e-06,
|
||||
"norm_num_groups": 32,
|
||||
"num_attention_heads": 32,
|
||||
"num_embeds_ada_norm": 1000,
|
||||
"num_layers": 28,
|
||||
"num_vector_embeds": None,
|
||||
"only_cross_attention": False,
|
||||
"out_channels": 128,
|
||||
"project_to_2d_pos": True,
|
||||
"upcast_attention": False,
|
||||
"use_linear_projection": False,
|
||||
"qk_norm": "rms_norm",
|
||||
"standardization_norm": "rms_norm",
|
||||
"positional_embedding_type": "rope",
|
||||
"positional_embedding_theta": 10000.0,
|
||||
"positional_embedding_max_pos": [20, 2048, 2048],
|
||||
"timestep_scale_multiplier": 1000,
|
||||
}
|
||||
if config is None:
|
||||
config = {
|
||||
"_class_name": "Transformer3DModel",
|
||||
"_diffusers_version": "0.25.1",
|
||||
"_name_or_path": "PixArt-alpha/PixArt-XL-2-256x256",
|
||||
"activation_fn": "gelu-approximate",
|
||||
"attention_bias": True,
|
||||
"attention_head_dim": 64,
|
||||
"attention_type": "default",
|
||||
"caption_channels": 4096,
|
||||
"cross_attention_dim": 2048,
|
||||
"double_self_attention": False,
|
||||
"dropout": 0.0,
|
||||
"in_channels": 128,
|
||||
"norm_elementwise_affine": False,
|
||||
"norm_eps": 1e-06,
|
||||
"norm_num_groups": 32,
|
||||
"num_attention_heads": 32,
|
||||
"num_embeds_ada_norm": 1000,
|
||||
"num_layers": 28,
|
||||
"num_vector_embeds": None,
|
||||
"only_cross_attention": False,
|
||||
"out_channels": 128,
|
||||
"project_to_2d_pos": True,
|
||||
"upcast_attention": False,
|
||||
"use_linear_projection": False,
|
||||
"qk_norm": "rms_norm",
|
||||
"standardization_norm": "rms_norm",
|
||||
"positional_embedding_type": "rope",
|
||||
"positional_embedding_theta": 10000.0,
|
||||
"positional_embedding_max_pos": [20, 2048, 2048],
|
||||
"timestep_scale_multiplier": 1000,
|
||||
}
|
||||
|
||||
transformer = Transformer3DModel.from_config(config)
|
||||
unet_prefix = "model.diffusion_model."
|
||||
@@ -142,8 +165,8 @@ class LTXVLoader:
|
||||
}
|
||||
)
|
||||
transformer.to(dtype).to(load_device).eval()
|
||||
|
||||
diffusion_model = LTXVTransformer3D(transformer, SymmetricPatchifier(1))
|
||||
patchifier = SymmetricPatchifier(1)
|
||||
diffusion_model = LTXVTransformer3D(transformer, patchifier, None, None, None)
|
||||
model = LTXVModel(
|
||||
LTXVModelConfig(num_latent_channels, dtype=dtype),
|
||||
model_type=comfy.model_base.ModelType.FLOW,
|
||||
|
||||
@@ -29,24 +29,25 @@ class LTXVModelConfig:
|
||||
|
||||
|
||||
class LTXVSampling(torch.nn.Module, comfy.model_sampling.CONST):
|
||||
def __init__(self, condition_mask):
|
||||
def __init__(self, condition_mask, guiding_latent=None):
|
||||
super().__init__()
|
||||
self.condition_mask = condition_mask
|
||||
self.guiding_latent = guiding_latent
|
||||
self.set_parameters(shift=1.0, multiplier=1)
|
||||
|
||||
def set_parameters(self, shift=1.0, timesteps=1000, multiplier=1000):
|
||||
self.shift = shift
|
||||
self.multiplier = multiplier
|
||||
ts = self.sigma((torch.arange(1, timesteps + 1, 1) / timesteps) * multiplier)
|
||||
ts = self.sigma((torch.arange(0, timesteps + 1, 1) / timesteps) * multiplier)
|
||||
self.register_buffer("sigmas", ts)
|
||||
|
||||
@property
|
||||
def sigma_min(self):
|
||||
return 0.0
|
||||
return self.sigmas[0]
|
||||
|
||||
@property
|
||||
def sigma_max(self):
|
||||
return 1.0
|
||||
return self.sigmas[-1]
|
||||
|
||||
def timestep(self, sigma):
|
||||
return sigma * self.multiplier
|
||||
@@ -61,6 +62,14 @@ class LTXVSampling(torch.nn.Module, comfy.model_sampling.CONST):
|
||||
return 0.0
|
||||
return 1.0 - percent
|
||||
|
||||
def calculate_input(self, sigma, noise):
|
||||
if self.guiding_latent is not None:
|
||||
noise = (
|
||||
noise * (1 - self.condition_mask)
|
||||
+ self.guiding_latent * self.condition_mask
|
||||
)
|
||||
return noise
|
||||
|
||||
def noise_scaling(self, sigma, noise, latent_image, max_denoise=False):
|
||||
self.condition_mask = self.condition_mask.to(latent_image.device)
|
||||
scaled = latent_image * (1 - sigma) + noise * sigma
|
||||
@@ -72,7 +81,15 @@ class LTXVSampling(torch.nn.Module, comfy.model_sampling.CONST):
|
||||
sigma = sigma.view(sigma.shape[:1] + (1,) * (model_output.ndim - 1))
|
||||
result = model_input - model_output * sigma
|
||||
# In order to d * dT to be zero in euler step, we need to set result equal to input in first latent frame.
|
||||
result = result * (1 - self.condition_mask) + model_input * self.condition_mask
|
||||
if self.guiding_latent is not None:
|
||||
result = (
|
||||
result * (1 - self.condition_mask)
|
||||
+ self.guiding_latent * self.condition_mask
|
||||
)
|
||||
else:
|
||||
result = (
|
||||
result * (1 - self.condition_mask) + model_input * self.condition_mask
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@@ -84,19 +101,53 @@ class LTXVModel(comfy.model_base.BaseModel):
|
||||
|
||||
class LTXVTransformer3D(nn.Module):
|
||||
def __init__(
|
||||
self, transformer: Transformer3DModel, patchifier: SymmetricPatchifier
|
||||
self,
|
||||
transformer: Transformer3DModel,
|
||||
patchifier: SymmetricPatchifier,
|
||||
conditioning_mask,
|
||||
latent_frame_rate,
|
||||
vae_scale_factor,
|
||||
):
|
||||
super().__init__()
|
||||
self.transformer = transformer
|
||||
self.dtype = transformer.dtype
|
||||
self.patchifier = patchifier
|
||||
|
||||
def forward(
|
||||
self.dtype = transformer.dtype
|
||||
self.transformer = transformer
|
||||
self.patchifier = patchifier
|
||||
self.conditioning_mask = conditioning_mask
|
||||
self.latent_frame_rate = latent_frame_rate
|
||||
self.vae_scale_factor = vae_scale_factor
|
||||
|
||||
def indices_grid(
|
||||
self,
|
||||
latent_shape,
|
||||
device,
|
||||
):
|
||||
use_rope = self.transformer.use_rope
|
||||
scale_grid = (
|
||||
(1 / self.latent_frame_rate, self.vae_scale_factor, self.vae_scale_factor)
|
||||
if use_rope
|
||||
else None
|
||||
)
|
||||
|
||||
indices_grid = self.patchifier.get_grid(
|
||||
orig_num_frames=latent_shape[2],
|
||||
orig_height=latent_shape[3],
|
||||
orig_width=latent_shape[4],
|
||||
batch_size=latent_shape[0],
|
||||
scale_grid=scale_grid,
|
||||
device=device,
|
||||
)
|
||||
|
||||
return indices_grid
|
||||
|
||||
def wrapped_transformer(
|
||||
self,
|
||||
latent,
|
||||
timesteps,
|
||||
context,
|
||||
indices_grid,
|
||||
skip_layer_mask=None,
|
||||
skip_layer_strategy=None,
|
||||
img_hw=None,
|
||||
aspect_ratio=None,
|
||||
mixed_precision=True,
|
||||
@@ -122,6 +173,8 @@ class LTXVTransformer3D(nn.Module):
|
||||
torch.int64
|
||||
),
|
||||
timestep=timesteps,
|
||||
skip_layer_mask=skip_layer_mask,
|
||||
skip_layer_strategy=skip_layer_strategy,
|
||||
return_dict=False,
|
||||
)[0]
|
||||
|
||||
@@ -134,50 +187,33 @@ class LTXVTransformer3D(nn.Module):
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
class LTXVTransformer3DWrapper(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
transformer: LTXVTransformer3D,
|
||||
patchifier: SymmetricPatchifier,
|
||||
conditioning_mask,
|
||||
indices_grid,
|
||||
):
|
||||
super().__init__()
|
||||
self.generator = torch.Generator(transformer.transformer.device).manual_seed(42)
|
||||
|
||||
self.indices_grid = indices_grid
|
||||
self.dtype = transformer.dtype
|
||||
self.wrapped_transformer = transformer
|
||||
self.patchifier = patchifier
|
||||
self.conditioning_mask = conditioning_mask
|
||||
self.conditioning_mask_patchified = patchifier.patchify(
|
||||
self.conditioning_mask
|
||||
).squeeze(-1)
|
||||
|
||||
def forward(self, x, timesteps, context, img_hw=None, aspect_ratio=None, **kwargs):
|
||||
transformer_options = kwargs.get("transformer_options", {})
|
||||
ptb_index = transformer_options.get("ptb_index", None)
|
||||
mixed_precision = transformer_options.get("mixed_precision", False)
|
||||
noise_scale = transformer_options.get("noise_scale", 0.15)
|
||||
mask = self.conditioning_mask_patchified.to(x.device)
|
||||
cond_or_uncond = transformer_options.get("cond_or_uncond", [])
|
||||
skip_block_list = transformer_options.get("skip_block_list", [])
|
||||
skip_layer_strategy = transformer_options.get("skip_layer_strategy", None)
|
||||
mask = self.patchifier.patchify(self.conditioning_mask).squeeze(-1).to(x.device)
|
||||
ndim_mask = mask.ndimension()
|
||||
expanded_timesteps = timesteps.view(timesteps.size(0), *([1] * (ndim_mask - 1)))
|
||||
timesteps_masked = expanded_timesteps * (1 - mask)
|
||||
skip_layer_mask = None
|
||||
if ptb_index is not None and ptb_index in cond_or_uncond:
|
||||
skip_layer_mask = self.transformer.create_skip_layer_mask(
|
||||
skip_block_list,
|
||||
1,
|
||||
len(cond_or_uncond),
|
||||
len(cond_or_uncond) - 1 - cond_or_uncond.index(ptb_index),
|
||||
)
|
||||
|
||||
noise = torch.randn(size=x.shape, device=x.device, generator=self.generator)
|
||||
|
||||
timesteps_unsqeezed = timesteps
|
||||
for _ in range(x.dim() - timesteps.dim()):
|
||||
timesteps_unsqeezed = timesteps_unsqeezed.unsqueeze(-1)
|
||||
latent = (
|
||||
x + noise_scale * noise * (timesteps_unsqeezed**2)
|
||||
) * self.conditioning_mask + x * (1 - self.conditioning_mask)
|
||||
|
||||
result = self.wrapped_transformer.forward(
|
||||
latent,
|
||||
result = self.wrapped_transformer(
|
||||
x,
|
||||
timesteps_masked,
|
||||
context,
|
||||
self.indices_grid,
|
||||
indices_grid=self.indices_grid(x.shape, x.device),
|
||||
mixed_precision=mixed_precision,
|
||||
skip_layer_mask=skip_layer_mask,
|
||||
skip_layer_strategy=skip_layer_strategy,
|
||||
)
|
||||
return result
|
||||
|
||||
+2
-1
@@ -9,7 +9,8 @@ requires-python = ">=3.10"
|
||||
readme = "README.md"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = [
|
||||
"ltx-video@git+https://github.com/Lightricks/LTX-Video@ltx-video-0.9",
|
||||
"ltx-video@git+https://github.com/Lightricks/LTX-Video@ltx-video-0.9.1",
|
||||
"av>=10.0.0"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
+2
-1
@@ -1 +1,2 @@
|
||||
ltx-video@git+https://github.com/Lightricks/LTX-Video@ltx-video-0.9
|
||||
av>=10.0.0
|
||||
ltx-video@git+https://github.com/Lightricks/LTX-Video@ltx-video-0.9.1
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import comfy.samplers
|
||||
import comfy.utils
|
||||
import torch
|
||||
from comfy.model_patcher import ModelPatcher
|
||||
from ltx_video.utils.skip_layer_strategy import SkipLayerStrategy
|
||||
|
||||
from .nodes_registry import comfy_node
|
||||
|
||||
|
||||
def stg(
|
||||
noise_pred_pos,
|
||||
noise_pred_neg,
|
||||
noise_pred_pertubed,
|
||||
cfg_scale,
|
||||
stg_scale,
|
||||
rescale_scale,
|
||||
):
|
||||
noise_pred = (
|
||||
noise_pred_neg
|
||||
+ cfg_scale * (noise_pred_pos - noise_pred_neg)
|
||||
+ stg_scale * (noise_pred_pos - noise_pred_pertubed)
|
||||
)
|
||||
if rescale_scale != 0:
|
||||
factor = noise_pred_pos.std() / noise_pred.std()
|
||||
factor = rescale_scale * factor + (1 - rescale_scale)
|
||||
noise_pred = noise_pred * factor
|
||||
return noise_pred
|
||||
|
||||
|
||||
@comfy_node(name="LTXVApplySTG")
|
||||
class LTXVApplySTG:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"model": (
|
||||
"MODEL",
|
||||
{"tooltip": "The model to apply the STG to."},
|
||||
),
|
||||
"stg_mode": (["attention", "residual"],),
|
||||
"block_indices": (
|
||||
"STRING",
|
||||
{
|
||||
"default": "14, 19",
|
||||
"tooltip": "Comma-separated indices of the blocks to apply the STG to.",
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
FUNCTION = "apply_stg"
|
||||
RETURN_TYPES = ("MODEL",)
|
||||
RETURN_NAMES = ("model",)
|
||||
CATEGORY = "lightricks/LTXV"
|
||||
|
||||
def apply_stg(self, model: ModelPatcher, stg_mode: str, block_indices: str):
|
||||
skip_block_list = [int(i.strip()) for i in block_indices.split(",")]
|
||||
stg_mode = (
|
||||
SkipLayerStrategy.Attention
|
||||
if stg_mode == "attention"
|
||||
else SkipLayerStrategy.Residual
|
||||
)
|
||||
new_model = model.clone()
|
||||
|
||||
new_model.model_options["transformer_options"]["skip_layer_strategy"] = stg_mode
|
||||
if "skip_block_list" in new_model.model_options["transformer_options"]:
|
||||
skip_block_list.extend(
|
||||
new_model.model_options["transformer_options"]["skip_block_list"]
|
||||
)
|
||||
new_model.model_options["transformer_options"][
|
||||
"skip_block_list"
|
||||
] = skip_block_list
|
||||
|
||||
return (new_model,)
|
||||
|
||||
|
||||
class STGGuider(comfy.samplers.CFGGuider):
|
||||
def set_conds(self, positive, negative):
|
||||
self.inner_set_conds(
|
||||
{"positive": positive, "negative": negative, "perturbed": positive}
|
||||
)
|
||||
|
||||
def set_cfg(self, cfg, stg_scale, rescale_scale: float = None):
|
||||
self.cfg = cfg
|
||||
self.stg_scale = stg_scale
|
||||
self.rescale_scale = rescale_scale
|
||||
|
||||
def predict_noise(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
timestep: torch.Tensor,
|
||||
model_options: dict = {},
|
||||
seed=None,
|
||||
):
|
||||
# in CFGGuider.predict_noise, we call sampling_function(), which uses cfg_function() to compute pos & neg
|
||||
# but we'd rather do a single batch of sampling pos, neg, and perturbed, so we call calc_cond_batch([perturbed,pos,neg]) directly
|
||||
|
||||
perturbed_cond = self.conds.get("perturbed", None)
|
||||
positive_cond = self.conds.get("positive", None)
|
||||
negative_cond = self.conds.get("negative", None)
|
||||
|
||||
noise_pred_neg = 0
|
||||
# no similar optimization for stg=0, use CFG guider instead.
|
||||
if self.cfg > 1:
|
||||
model_options["transformer_options"]["ptb_index"] = 2
|
||||
(noise_pred_perturbed, noise_pred_pos, noise_pred_neg) = (
|
||||
comfy.samplers.calc_cond_batch(
|
||||
self.inner_model,
|
||||
[perturbed_cond, positive_cond, negative_cond],
|
||||
x,
|
||||
timestep,
|
||||
model_options,
|
||||
)
|
||||
)
|
||||
else:
|
||||
model_options["transformer_options"]["ptb_index"] = 1
|
||||
(noise_pred_perturbed, noise_pred_pos) = comfy.samplers.calc_cond_batch(
|
||||
self.inner_model,
|
||||
[perturbed_cond, positive_cond],
|
||||
x,
|
||||
timestep,
|
||||
model_options,
|
||||
)
|
||||
stg_result = stg(
|
||||
noise_pred_pos,
|
||||
noise_pred_neg,
|
||||
noise_pred_perturbed,
|
||||
self.cfg,
|
||||
self.stg_scale,
|
||||
self.rescale_scale,
|
||||
)
|
||||
|
||||
# normally this would be done in cfg_function, but we skipped
|
||||
# that for efficiency: we can compute the noise predictions in
|
||||
# a single call to calc_cond_batch() (rather than two)
|
||||
# so we replicate the hook here
|
||||
for fn in model_options.get("sampler_post_cfg_function", []):
|
||||
args = {
|
||||
"denoised": stg_result,
|
||||
"cond": positive_cond,
|
||||
"uncond": negative_cond,
|
||||
"model": self.inner_model,
|
||||
"uncond_denoised": noise_pred_neg,
|
||||
"cond_denoised": noise_pred_pos,
|
||||
"sigma": timestep,
|
||||
"model_options": model_options,
|
||||
"input": x,
|
||||
# not in the original call in samplers.py:cfg_function, but made available for future hooks
|
||||
"perturbed_cond": positive_cond,
|
||||
"perturbed_cond_denoised": noise_pred_perturbed,
|
||||
}
|
||||
stg_result = fn(args)
|
||||
|
||||
return stg_result
|
||||
|
||||
|
||||
@comfy_node(name="STGGuider")
|
||||
class STGGuiderNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"model": ("MODEL",),
|
||||
"positive": ("CONDITIONING",),
|
||||
"negative": ("CONDITIONING",),
|
||||
"cfg": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 100.0,
|
||||
"step": 0.1,
|
||||
"round": 0.01,
|
||||
},
|
||||
),
|
||||
"stg": (
|
||||
"FLOAT",
|
||||
{"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.01},
|
||||
),
|
||||
"rescale": (
|
||||
"FLOAT",
|
||||
{"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.01},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("GUIDER",)
|
||||
|
||||
FUNCTION = "get_guider"
|
||||
CATEGORY = "lightricks/LTXV"
|
||||
|
||||
def get_guider(self, model, positive, negative, cfg, stg, rescale):
|
||||
guider = STGGuider(model)
|
||||
guider.set_conds(positive, negative)
|
||||
guider.set_cfg(cfg, stg, rescale)
|
||||
return (guider,)
|
||||
+36
-59
@@ -9,10 +9,9 @@ import comfy.supported_models_base
|
||||
import comfy.utils
|
||||
import torch
|
||||
from ltx_video.models.autoencoders.vae_encode import get_vae_size_scale_factor
|
||||
from ltx_video.models.transformers.symmetric_patchifier import SymmetricPatchifier
|
||||
|
||||
from .img2vid import encode_media_conditioning
|
||||
from .model import LTXVSampling, LTXVTransformer3DWrapper
|
||||
from .model import LTXVSampling
|
||||
from .nodes_registry import comfy_node
|
||||
|
||||
|
||||
@@ -88,14 +87,13 @@ class LTXVModelConfigurator:
|
||||
"frame_rate": ("INT", {"default": 25, "min": 1, "max": 60}),
|
||||
"batch": ("INT", {"default": 1, "min": 1, "max": 60}),
|
||||
"mixed_precision": ("BOOLEAN", {"default": True}),
|
||||
"noise_scale": (
|
||||
"FLOAT",
|
||||
"img_compression": (
|
||||
"INT",
|
||||
{
|
||||
"default": 0.15,
|
||||
"min": 0.0,
|
||||
"default": 29,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 0.01,
|
||||
"tooltip": "Amount of noise to apply on conditioning image latent.",
|
||||
"tooltip": "Amount of compression to apply on conditioning image.",
|
||||
},
|
||||
),
|
||||
},
|
||||
@@ -103,7 +101,13 @@ class LTXVModelConfigurator:
|
||||
"conditioning": (
|
||||
"IMAGE",
|
||||
{"tooltip": "Optional conditioning image or video."},
|
||||
)
|
||||
),
|
||||
"initial_latent": (
|
||||
"LATENT",
|
||||
{
|
||||
"tooltip": "initial latent that is combined with conditioning if given"
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -137,32 +141,6 @@ class LTXVModelConfigurator:
|
||||
]
|
||||
return latent_shape, latent_frame_rate
|
||||
|
||||
def indices_grid(
|
||||
self,
|
||||
latent_shape,
|
||||
vae_scale_factor,
|
||||
latent_frame_rate,
|
||||
patchifier,
|
||||
use_rope,
|
||||
device,
|
||||
):
|
||||
scale_grid = (
|
||||
(1 / latent_frame_rate, vae_scale_factor, vae_scale_factor)
|
||||
if use_rope # self.wrapped_transformer.transformer.use_rope
|
||||
else None
|
||||
)
|
||||
|
||||
indices_grid = patchifier.get_grid(
|
||||
orig_num_frames=latent_shape[2],
|
||||
orig_height=latent_shape[3],
|
||||
orig_width=latent_shape[4],
|
||||
batch_size=latent_shape[0],
|
||||
scale_grid=scale_grid,
|
||||
device=device,
|
||||
)
|
||||
|
||||
return indices_grid
|
||||
|
||||
def configure_sizes(
|
||||
self,
|
||||
model,
|
||||
@@ -174,8 +152,9 @@ class LTXVModelConfigurator:
|
||||
frame_rate,
|
||||
batch,
|
||||
mixed_precision,
|
||||
noise_scale,
|
||||
img_compression,
|
||||
conditioning=None,
|
||||
initial_latent=None,
|
||||
):
|
||||
load_device = comfy.model_management.get_torch_device()
|
||||
if preset != "Custom":
|
||||
@@ -193,43 +172,41 @@ class LTXVModelConfigurator:
|
||||
latent_shape[4],
|
||||
]
|
||||
conditioning_mask = torch.zeros(mask_shape, device=load_device)
|
||||
initial_latent = (
|
||||
None
|
||||
if initial_latent is None
|
||||
else initial_latent["samples"].to(load_device)
|
||||
)
|
||||
guiding_latent = None
|
||||
if conditioning is not None:
|
||||
latent = encode_media_conditioning(
|
||||
conditioning, vae, width, height, frames_number
|
||||
conditioning,
|
||||
vae,
|
||||
width,
|
||||
height,
|
||||
frames_number,
|
||||
image_compression=img_compression,
|
||||
initial_latent=initial_latent,
|
||||
)
|
||||
conditioning_mask[:, :, 0] = 1.0
|
||||
guiding_latent = latent[:, :, :1, ...]
|
||||
else:
|
||||
latent = torch.zeros(latent_shape, dtype=torch.float32)
|
||||
latent = torch.zeros(latent_shape, dtype=torch.float32, device=load_device)
|
||||
if initial_latent is not None:
|
||||
latent[:, :, : initial_latent.shape[2], ...] = initial_latent
|
||||
|
||||
patchifier = SymmetricPatchifier(1)
|
||||
_, vae_scale_factor, _ = get_vae_size_scale_factor(vae.first_stage_model)
|
||||
use_rope = model.get_model_object("diffusion_model").transformer.use_rope
|
||||
indices_grid = self.indices_grid(
|
||||
latent_shape,
|
||||
vae_scale_factor,
|
||||
latent_frame_rate,
|
||||
patchifier,
|
||||
use_rope,
|
||||
load_device,
|
||||
)
|
||||
wrapper = LTXVTransformer3DWrapper(
|
||||
transformer=model.get_model_object("diffusion_model"),
|
||||
patchifier=patchifier,
|
||||
conditioning_mask=conditioning_mask,
|
||||
indices_grid=indices_grid,
|
||||
)
|
||||
|
||||
patcher = model.clone()
|
||||
patcher.add_object_patch("diffusion_model", wrapper)
|
||||
patcher.add_object_patch("diffusion_model.conditioning_mask", conditioning_mask)
|
||||
patcher.add_object_patch("diffusion_model.latent_frame_rate", latent_frame_rate)
|
||||
patcher.add_object_patch("diffusion_model.vae_scale_factor", vae_scale_factor)
|
||||
patcher.add_object_patch(
|
||||
"model_sampling", LTXVSampling(wrapper.conditioning_mask)
|
||||
"model_sampling", LTXVSampling(conditioning_mask, guiding_latent)
|
||||
)
|
||||
patcher.model_options.setdefault("transformer_options", {})[
|
||||
"mixed_precision"
|
||||
] = mixed_precision
|
||||
patcher.model_options.setdefault("transformer_options", {})["noise_scale"] = (
|
||||
noise_scale if conditioning is not None else 0.0
|
||||
)
|
||||
|
||||
num_latent_patches = latent_shape[2] * latent_shape[3] * latent_shape[4]
|
||||
return (patcher, {"samples": latent}, get_normal_shift(num_latent_patches))
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from copy import copy
|
||||
|
||||
import comfy.latent_formats
|
||||
import comfy.model_base
|
||||
import comfy.model_management
|
||||
@@ -13,11 +15,16 @@ from ltx_video.models.autoencoders.vae_encode import (
|
||||
vae_encode,
|
||||
)
|
||||
|
||||
from .nodes_registry import comfy_node
|
||||
|
||||
|
||||
class LTXVVAE(comfy.sd.VAE):
|
||||
def __init__(self):
|
||||
def __init__(self, decode_timestep=0.05, decode_noise_scale=0.025, seed=42):
|
||||
self.device = comfy.model_management.vae_device()
|
||||
self.offload_device = comfy.model_management.vae_offload_device()
|
||||
self.decode_timestep = decode_timestep
|
||||
self.decode_noise_scale = decode_noise_scale
|
||||
self.seed = seed
|
||||
|
||||
@classmethod
|
||||
def from_pretrained(cls, vae_class, model_path, dtype=torch.bfloat16):
|
||||
@@ -53,18 +60,48 @@ class LTXVVAE(comfy.sd.VAE):
|
||||
# (batch, channels, frames, height, width)
|
||||
def decode(self, samples_in):
|
||||
is_video = samples_in.shape[2] > 1
|
||||
decode_timestep = self.decode_timestep
|
||||
if getattr(self.first_stage_model.decoder, "timestep_conditioning", False):
|
||||
samples_in = self.add_noise(
|
||||
decode_timestep, self.decode_noise_scale, self.seed, samples_in
|
||||
)
|
||||
else:
|
||||
decode_timestep = None
|
||||
|
||||
result = vae_decode(
|
||||
samples_in.to(self.device),
|
||||
vae=self.first_stage_model,
|
||||
is_video=is_video,
|
||||
vae_per_channel_normalize=True,
|
||||
timestep=decode_timestep,
|
||||
)
|
||||
result = self.image_processor.postprocess(
|
||||
result, output_type="pt", do_denormalize=[True]
|
||||
)
|
||||
return (
|
||||
result.squeeze(0).permute(1, 2, 3, 0).to(torch.float32)
|
||||
) # .to(self.offload_device)
|
||||
return result.squeeze(0).permute(1, 2, 3, 0).to(torch.float32)
|
||||
|
||||
@staticmethod
|
||||
def add_noise(decode_timestep, decode_noise_scale, seed, latents):
|
||||
generator = torch.Generator(device="cpu").manual_seed(seed)
|
||||
noise = torch.randn(
|
||||
latents.size(),
|
||||
generator=generator,
|
||||
device=latents.device,
|
||||
dtype=latents.dtype,
|
||||
)
|
||||
if not isinstance(decode_timestep, list):
|
||||
decode_timestep = [decode_timestep] * latents.shape[0]
|
||||
if decode_noise_scale is None:
|
||||
decode_noise_scale = decode_timestep
|
||||
elif not isinstance(decode_noise_scale, list):
|
||||
decode_noise_scale = [decode_noise_scale] * latents.shape[0]
|
||||
|
||||
decode_timestep = torch.tensor(decode_timestep).to(latents.device)
|
||||
decode_noise_scale = torch.tensor(decode_noise_scale).to(latents.device)[
|
||||
:, None, None, None, None
|
||||
]
|
||||
latents = latents * (1 - decode_noise_scale) + noise * decode_noise_scale
|
||||
return latents
|
||||
|
||||
# Underlying VAE expects b, c, n, h, w dimensions order and dtype specific dtype.
|
||||
# However in Comfy the convension is n, h, w, c.
|
||||
@@ -77,3 +114,54 @@ class LTXVVAE(comfy.sd.VAE):
|
||||
input, self.first_stage_model, vae_per_channel_normalize=True
|
||||
).to(comfy.model_management.get_torch_device())
|
||||
return latents
|
||||
|
||||
|
||||
@comfy_node(name="Add VAE Decoder Noise")
|
||||
class DecoderNoise:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"vae": ("VAE",),
|
||||
"timestep": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.05,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01,
|
||||
"tooltip": "The timestep used for decoding the noise.",
|
||||
},
|
||||
),
|
||||
"scale": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.025,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.001,
|
||||
"tooltip": "The scale of the noise added to the decoder.",
|
||||
},
|
||||
),
|
||||
"seed": (
|
||||
"INT",
|
||||
{
|
||||
"default": 42,
|
||||
"min": 0,
|
||||
"max": 0xFFFFFFFFFFFFFFFF,
|
||||
"tooltip": "The random seed used for creating the noise.",
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
FUNCTION = "add_noise"
|
||||
RETURN_TYPES = ("VAE",)
|
||||
CATEGORY = "lightricks/LTXV"
|
||||
|
||||
def add_noise(self, vae, timestep, scale, seed):
|
||||
result = copy(vae)
|
||||
result.decode_timestep = timestep
|
||||
result.decode_noise_scale = scale
|
||||
result.seed = seed
|
||||
return (result,)
|
||||
|
||||
Reference in New Issue
Block a user