Compare commits
324
Commits
audio_format
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
973075279e | ||
|
|
caa5c1c440 | ||
|
|
d060c20ff5 | ||
|
|
249bb50669 | ||
|
|
6394f809b8 | ||
|
|
4ee72c065d | ||
|
|
a6879b8388 | ||
|
|
2984ec4c4b | ||
|
|
449839959f | ||
|
|
59529f50e6 | ||
|
|
993082e4f2 | ||
|
|
c67ff3ef37 | ||
|
|
3234937ff5 | ||
|
|
537f3a0226 | ||
|
|
4f804bb23f | ||
|
|
b9cc3f23aa | ||
|
|
2571e60a1c | ||
|
|
4bc8d33127 | ||
|
|
a206b3443c | ||
|
|
f2a18a1f86 | ||
|
|
ae85dc37e6 | ||
|
|
8923bd836b | ||
|
|
8550981384 | ||
|
|
a02bc130f8 | ||
|
|
6e7f638675 | ||
|
|
08e8df15db | ||
|
|
0edce8ef7c | ||
|
|
8450df1a8b | ||
|
|
1355e8c69f | ||
|
|
dabda5423a | ||
|
|
2372cebde1 | ||
|
|
8e4d79471b | ||
|
|
f20b356f68 | ||
|
|
7e9bea5336 | ||
|
|
c017071cc3 | ||
|
|
9cdacf6363 | ||
|
|
330bce6c3c | ||
|
|
f1c513a563 | ||
|
|
9c5407f2a8 | ||
|
|
eb36470be4 | ||
|
|
9601d8792e | ||
|
|
97a57e38a8 | ||
|
|
285568f530 | ||
|
|
be11f43838 | ||
|
|
253251fa5c | ||
|
|
f01332d42c | ||
|
|
229642349b | ||
|
|
3b2fb5f518 | ||
|
|
a3d66b6736 | ||
|
|
8b62aa458d | ||
|
|
7a50ec7ec8 | ||
|
|
f76436dcf2 | ||
|
|
f1698cbd92 | ||
|
|
9e963ff5ed | ||
|
|
266f27aeba | ||
|
|
dc98c8be65 | ||
|
|
66e443b583 | ||
|
|
66e95e9471 | ||
|
|
7b26c04c8a | ||
|
|
d7c47abc5e | ||
|
|
688b796e58 | ||
|
|
a5946e88a2 | ||
|
|
a20beefa20 | ||
|
|
63bfea3d74 | ||
|
|
d5ba30a754 | ||
|
|
2ab65b9c4c | ||
|
|
c953e12408 | ||
|
|
6ec6007553 | ||
|
|
0d5b445a70 | ||
|
|
38fbe354b1 | ||
|
|
001a508cb5 | ||
|
|
a7ce59e381 | ||
|
|
598e181e97 | ||
|
|
47fb44708d | ||
|
|
69ef6d4edc | ||
|
|
7ed4f6c510 | ||
|
|
0c0288f9b5 | ||
|
|
e49fed4585 | ||
|
|
053e2beb1d | ||
|
|
87f73c7dbb | ||
|
|
136fc6abde | ||
|
|
82f62dcbf4 | ||
|
|
a11bcf4a08 | ||
|
|
17ad06aa25 | ||
|
|
4980590ac1 | ||
|
|
df55f01d1d | ||
|
|
7236c5dff0 | ||
|
|
2fb06b1634 | ||
|
|
d1733bb518 | ||
|
|
a0312ec4c2 | ||
|
|
b9366c11bc | ||
|
|
ab96cf2608 | ||
|
|
f837d6ffdd | ||
|
|
20c4c5a40d | ||
|
|
ccbaf837ad | ||
|
|
eeb794ab0d | ||
|
|
d18ff47633 | ||
|
|
b34d3ba46b | ||
|
|
972c87da57 | ||
|
|
0a3b1eb433 | ||
|
|
c9dcc3a229 | ||
|
|
d3e3c2e6b4 | ||
|
|
546359d436 | ||
|
|
0d5870a4ce | ||
|
|
5e61bcf218 | ||
|
|
c338a1e572 | ||
|
|
bbac2480f7 | ||
|
|
2fbbd53e62 | ||
|
|
93ae745f22 | ||
|
|
e661ee7f3f | ||
|
|
63ac6727c9 | ||
|
|
718af34bc2 | ||
|
|
0a75c7958f | ||
|
|
3b36daba92 | ||
|
|
53c406679d | ||
|
|
e44b3565c7 | ||
|
|
2cf03c6ac0 | ||
|
|
73418575ca | ||
|
|
ea525bfbd6 | ||
|
|
28eb44c13c | ||
|
|
1ad84c1a11 | ||
|
|
25c31ca79e | ||
|
|
031c4ae301 | ||
|
|
3a96e87951 | ||
|
|
7e3eb2a62e | ||
|
|
626cb249d1 | ||
|
|
4c7858ddd5 | ||
|
|
51aa43368d | ||
|
|
b601e8cf61 | ||
|
|
8343122234 | ||
|
|
532de499f6 | ||
|
|
23ebe28091 | ||
|
|
2c25b8b538 | ||
|
|
514de7fe2e | ||
|
|
f2c905aaed | ||
|
|
127b3646fa | ||
|
|
9e0590cfdb | ||
|
|
124c913ccd | ||
|
|
8262bb941d | ||
|
|
c36626c602 | ||
|
|
c81556ac3c | ||
|
|
24566553a5 | ||
|
|
082b2007f0 | ||
|
|
8ffcde28b2 | ||
|
|
e8ad2a851d | ||
|
|
0963fa917f | ||
|
|
8629188458 | ||
|
|
3bfbd99ffc | ||
|
|
701f7e7c59 | ||
|
|
f470aeb38e | ||
|
|
7895509345 | ||
|
|
ffbc273042 | ||
|
|
86e9f6a4cb | ||
|
|
f736938962 | ||
|
|
5d0c595df3 | ||
|
|
34daa91e0e | ||
|
|
0a4462bb1e | ||
|
|
38927c5f46 | ||
|
|
7141535f22 | ||
|
|
903ecc3a97 | ||
|
|
76d9fe74f3 | ||
|
|
b212f7a3bd | ||
|
|
338c3ef61e | ||
|
|
bc5c3d134b | ||
|
|
5d98b154a8 | ||
|
|
17fc1b9b67 | ||
|
|
c2449e03dd | ||
|
|
d03988238d | ||
|
|
565208bfe0 | ||
|
|
a5e362b87e | ||
|
|
1b35b85f95 | ||
|
|
616577a0bd | ||
|
|
460c90afd1 | ||
|
|
17abd98d99 | ||
|
|
b85ead786c | ||
|
|
775b931f64 | ||
|
|
2498563e23 | ||
|
|
98e651bc91 | ||
|
|
9a1ff45106 | ||
|
|
f3f78c4074 | ||
|
|
f24f4e10f4 | ||
|
|
a54edeaf7b | ||
|
|
8e1ccb21f6 | ||
|
|
48e7f70418 | ||
|
|
c47b10ca17 | ||
|
|
c5216a51c0 | ||
|
|
78753dbbdb | ||
|
|
17f25aec65 | ||
|
|
875febd153 | ||
|
|
3ed5c7fcc2 | ||
|
|
23d3de9084 | ||
|
|
cad87a17a3 | ||
|
|
5f118cc5b8 | ||
|
|
9699e3e8cd | ||
|
|
3d584a1996 | ||
|
|
9951b2447d | ||
|
|
42f52a6a91 | ||
|
|
43ac76cffb | ||
|
|
4a72c8354d | ||
|
|
d54eed673a | ||
|
|
7036292563 | ||
|
|
ccde0f2014 | ||
|
|
25eeb7168c | ||
|
|
6953fa2144 | ||
|
|
e5d8fd3033 | ||
|
|
bdcb974a6e | ||
|
|
84b469a691 | ||
|
|
2d8feb1019 | ||
|
|
0eeb3a4a8d | ||
|
|
24de9ef249 | ||
|
|
0d44f4b46d | ||
|
|
bdd98085ad | ||
|
|
eb39ab47cf | ||
|
|
97e8e2ebed | ||
|
|
a9d08964ad | ||
|
|
2b15c4f03c | ||
|
|
f4b4764694 | ||
|
|
c8ec04b782 | ||
|
|
05f085e97a | ||
|
|
82229859fe | ||
|
|
b0f9796ea3 | ||
|
|
fb56b2db7c | ||
|
|
8a94370cf5 | ||
|
|
f5469b1caa | ||
|
|
9107873f31 | ||
|
|
8893cdc8be | ||
|
|
4b63e41bce | ||
|
|
cc49596040 | ||
|
|
3796aab7d6 | ||
|
|
d1d59ba6ac | ||
|
|
c00ce8e857 | ||
|
|
9d608e7a6a | ||
|
|
9f3b0d686e | ||
|
|
e2cd11dd1f | ||
|
|
c36cceb511 | ||
|
|
81a1645e8a | ||
|
|
d08866b1b8 | ||
|
|
fdb71637b7 | ||
|
|
ad7255ae0b | ||
|
|
742e79331d | ||
|
|
de647799b5 | ||
|
|
746b0e62bd | ||
|
|
7cc5061938 | ||
|
|
0376e57744 | ||
|
|
1e54a41f10 | ||
|
|
ae51c330ef | ||
|
|
9700a6c0bf | ||
|
|
f3eadf9b99 | ||
|
|
0cdc93273e | ||
|
|
847a24f69c | ||
|
|
54a38cb963 | ||
|
|
14443ebdfc | ||
|
|
60b9bedf27 | ||
|
|
76ddf5b676 | ||
|
|
adcf31fac8 | ||
|
|
b471f134ed | ||
|
|
6e8a53b3e6 | ||
|
|
a4135a719a | ||
|
|
ad6b13a3a3 | ||
|
|
8cc4138a48 | ||
|
|
0a26e2defd | ||
|
|
c0b9fc90ab | ||
|
|
21344549fe | ||
|
|
d2cf5d7b03 | ||
|
|
ccf95b92ea | ||
|
|
289d7d8d99 | ||
|
|
6bffe8b90f | ||
|
|
5c7455a1c8 | ||
|
|
5333656fe7 | ||
|
|
ca04cd3ac3 | ||
|
|
d917f01297 | ||
|
|
35f372c808 | ||
|
|
7dc2a03f9b | ||
|
|
9dff4d6500 | ||
|
|
bf2a9402d0 | ||
|
|
41ca4439e1 | ||
|
|
8140415680 | ||
|
|
a6ba37ee28 | ||
|
|
1ae5379473 | ||
|
|
156164c331 | ||
|
|
4664081a2a | ||
|
|
8b9df52180 | ||
|
|
e4d7c30ac3 | ||
|
|
013473335a | ||
|
|
5aa08cb61b | ||
|
|
0e0bcc72f8 | ||
|
|
e667990015 | ||
|
|
8e774e44ce | ||
|
|
9e841a5b88 | ||
|
|
bd077e42b6 | ||
|
|
12b9f8d00f | ||
|
|
8a564019ef | ||
|
|
3dc15c4507 | ||
|
|
3489bccd20 | ||
|
|
bb7186aae4 | ||
|
|
b253112553 | ||
|
|
8f8df32db1 | ||
|
|
fc328d88ce | ||
|
|
7c313fd9fb | ||
|
|
9405e803c2 | ||
|
|
990e6e2448 | ||
|
|
3a906cfe8f | ||
|
|
a0f616cbef | ||
|
|
1e9f7e7257 | ||
|
|
d6ac0b999b | ||
|
|
e1c4e939ec | ||
|
|
70faa9bcef | ||
|
|
7b934a81c7 | ||
|
|
623a603ed3 | ||
|
|
3b7492d023 | ||
|
|
d6cf89c501 | ||
|
|
bd5381ec8f | ||
|
|
b5d907bade | ||
|
|
8c50452255 | ||
|
|
48edad3aea | ||
|
|
4838634372 | ||
|
|
ce5d0a7548 | ||
|
|
29fae7510f | ||
|
|
962e8e86ef | ||
|
|
c7b1dc7328 | ||
|
|
0037f49aee | ||
|
|
3258f3cac5 | ||
|
|
0be3bc0597 | ||
|
|
23d822c423 |
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="640" viewBox="0 0 1280 640" role="img" aria-label="Hanzo-VideoHelper">
|
||||
<rect width="1280" height="640" fill="#0A0A0A"/>
|
||||
<svg x="96" y="215" width="210" height="210" viewBox="0 0 67 67"><path d="M22.21 67V44.6369H0V67H22.21Z" fill="#fff"/><path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#fff"/><path d="M22.21 0H0V22.3184H22.21V0Z" fill="#fff"/><path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#fff"/><path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#fff"/></svg>
|
||||
<text x="378" y="276" font-family="Inter,system-ui,-apple-system,sans-serif" font-size="78" font-weight="800" letter-spacing="-2" fill="#ffffff">Hanzo-VideoHelper</text>
|
||||
<text x="378" y="322" font-family="Inter,system-ui,sans-serif" font-size="30" fill="#ffffff" opacity=".66">Nodes related to video workflows</text>
|
||||
<rect x="378" y="338" width="806" height="3" rx="1.5" fill="#ffffff" opacity=".9"/>
|
||||
<text x="378" y="390" font-family="Inter,system-ui,sans-serif" font-size="24" font-weight="600" fill="#ffffff" opacity=".5">github.com/hanzoai</text>
|
||||
<text x="1184" y="390" text-anchor="end" font-family="Inter,system-ui,sans-serif" font-size="24" font-weight="600" fill="#ffffff" opacity=".5">hanzo.ai</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -7,14 +7,18 @@ on:
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'Kosinkadink' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@main
|
||||
uses: Comfy-Org/publish-node-action@v1
|
||||
with:
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} ## Add your own personal access token to your Github Repository secrets and reference it here.
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} ## Add your own personal access token to your Github Repository secrets and reference it here.
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Hanzo-VideoHelper
|
||||
|
||||
**Org:** hanzoai · **Ecosystem:** hanzo · **Path:** `/Users/a/work/hanzo/hanzoai/Hanzo-VideoHelper`
|
||||
**Origin:** https://github.com/hanzoai/Hanzo-VideoHelper.git
|
||||
|
||||
## Discovery
|
||||
|
||||
This file (`CLAUDE.md`) is the canonical agent-facing readme; `LLM.md` is a symlink to it. Update either name and both stay in sync.
|
||||
|
||||
## Where to look first
|
||||
|
||||
- `README.md` — human-facing overview (if present)
|
||||
- `package.json` / `Cargo.toml` / `pyproject.toml` / `go.mod` — language & deps
|
||||
- `.github/workflows/` — CI surface
|
||||
- `docs/` — extended docs (if present)
|
||||
|
||||
## Sibling repos
|
||||
|
||||
See the org-level `LLM.md` at `/Users/a/work/hanzo/hanzoai/LLM.md` for the full inventory of sibling repos and inter-repo dependencies.
|
||||
@@ -1,110 +1,79 @@
|
||||
# ComfyUI-VideoHelperSuite
|
||||
Nodes related to video workflows
|
||||
<p align="center"><img src=".github/hero.svg" alt="Hanzo-VideoHelper" width="880"></p>
|
||||
|
||||
## I/O Nodes
|
||||
### Load Video
|
||||
Converts a video file into a series of images
|
||||
- video: The video file to be loaded
|
||||
- force_rate: Discards or duplicates frames as needed to hit a target frame rate. Disabled by setting to 0. This can be used to quickly match a suggested frame rate like the 8 fps of AnimateDiff.
|
||||
- force_size: Allows for quick resizing to a number of suggested sizes. Several options allow you to set only width or height and determine the other from aspect ratio.
|
||||
- frame_load_cap: The maximum number of frames which will be returned. This could also be thought of as the maximum batch size.
|
||||
- skip_first_frames: How many frames to skip from the start of the video after adjusting for a forced frame rate. By incrementing this number by the frame_load_cap, you can easily process a longer input video in parts.
|
||||
- select_every_nth: Allows for skipping a number of frames without considering the base frame rate or risking frame duplication. Often useful when working with animated gifs
|
||||
A path variant of the Load Video node exists that allows loading videos from external paths
|
||||

|
||||

|
||||
If [Advanced Previews](#advanced-previews) is enabled in the options menu of the web ui, the preview will reflect the current settings on the node.
|
||||
### Load Image Sequence
|
||||
Loads all image files from a subfolder. Options are similar to Load Video.
|
||||
- image_load_cap: The maximum number of images which will be returned. This could also be thought of as the maximum batch size.
|
||||
- skip_first_images: How many images to skip. By incrementing this number by image_load_cap, you can easily divide a long sequence of images into multiple batches.
|
||||
- select_every_nth: Allows for skipping a number of images between every returned frame.
|
||||
# Hanzo-VideoHelper
|
||||
|
||||
A path variant of Load Image sequence also exists.
|
||||
### Video Combine
|
||||
Combines a series of images into an output video
|
||||
If the optional audio input is provided, it will also be combined into the output video
|
||||
- frame_rate: How many of the input frames are displayed per second. A higher frame rate means that the output video plays faster and has less duration. This should usually be kept to 8 for AnimateDiff, or matched to the force_rate of a Load Video node.
|
||||
- loop_count: How many additional times the video should repeat
|
||||
- filename_prefix: The base file name used for output.
|
||||
- You can save output to a subfolder: `subfolder/video`
|
||||
- Like the builtin Save Image node, you can add timestamps. `%date:yyyy-MM-ddThh:mm:ss%` might become 2023-10-31T6:45:25
|
||||
- format: The file format to use. Advanced information on configuring or adding additional video formats can be found in the [Video Formats](#video-formats) section.
|
||||
- pingpong: Causes the input to be played back in the reverse to create a clean loop.
|
||||
- save_output: Whether the image should be put into the output directory or the temp directory.
|
||||
Returns: a `VHS_FILENAMES` which consists of a boolean indicating if save_output is enabled and a list of the full filepaths of all generated outputs in the order created. Accordingly `output[1][-1]` will be the most complete output.
|
||||
|
||||
Depending on the format chosen, additional options may become available, including
|
||||
- crf: Describes the quality of the output video. A lower number gives a higher quality video and a larger file size, while a higher number gives a lower quality video with a smaller size. Scaling varies by codec, but visually lossless output generally occurs around 20.
|
||||
- save_metadata: Includes a copy of the workflow in the ouput video which can be loaded by dragging and dropping the video, just like with images.
|
||||
- pix_fmt: Changes how the pixel data is stored. `yuv420p10le` has higher color quality, but won't work on all devices
|
||||
### Load Audio
|
||||
Provides a way to load standalone audio files.
|
||||
- seek_seconds: An optional start time for the audio file in seconds.
|
||||
**Video I/O suite for the Hanzo ecosystem**
|
||||
|
||||
## Latent/Image Nodes
|
||||
A number of utility nodes exist for managing latents. For each, there is an equivalent node which works on images.
|
||||
### Split Batch
|
||||
Divides the latents into two sets. The first `split_index` latents go to ouput A and the remainder to output B. If less then `split_index` latents are provided as input, all are passed to output A and output B is empty.
|
||||
### Merge Batch
|
||||
Combines two groups of latents into a single output. The order of the output is the latents in A followed by the latents in B.
|
||||
If the input groups are not the same size, the node provides options for rescaling the latents before merging.
|
||||
### Select Every Nth
|
||||
The first of every `select_every_nth` input is passed and the remainder are discarded
|
||||
### Get Count
|
||||
### Duplicate Batch
|
||||
Part of [Hanzo Painter](https://github.com/hanzoai/painter) - AI-powered watermark removal and video inpainting platform.
|
||||
|
||||
## Video Previews
|
||||
Load Video (Upload), Load Video (Path), Load Images (Upload), Load Images (Path) and Video Combine provide animated previews.
|
||||
Nodes with previews provide additional functionality when right clicked
|
||||
- Open preview
|
||||
- Save preview
|
||||
- Pause preview: Can improve performance with very large videos
|
||||
- Hide preview: Can improve performance, save space
|
||||
- Sync preview: Restarts all previews for side-by-side comparisons
|
||||
[](https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite)
|
||||
[](https://hanzo.ai)
|
||||
|
||||
### Advanced Previews
|
||||
Advanced Previews must be manually enabled by clicking the settings gear next to Queue Prompt and checking the box for VHS Advanced Previews.
|
||||
If enabled, videos which are displayed in the ui will be converted with ffmpeg on request. This has several benefits
|
||||
- Previews for Load Video nodes will reflect the settings on the node such as skip_first_frames and frame_load_cap
|
||||
- This makes it easy to select an exact portion of an input video and sync it with outputs
|
||||
- It can use substantially less bandwidth if running the server remotely
|
||||
- It can greatly improve the browser performance by downsizing videos to the in ui resolution, particularly useful with animated gifs
|
||||
- It allows for previews of videos that would not normally be playable in browser.
|
||||
- Can be limited to subdirectories of ComyUI if `VHS_STRICT_PATHS` is set as an environment variable.
|
||||
## About
|
||||
|
||||
This fucntionality is disabled since it comes with several downsides
|
||||
- There is a delay before videos show in the browser. This delay can become quite large if the input video is long
|
||||
- The preview videos are lower quality (The original can always be viewed with Right Click -> Open preview)
|
||||
Hanzo-VideoHelper is a Hanzo-maintained fork of Kosinkadink/ComfyUI-VideoHelperSuite, providing Video I/O suite for video processing. This fork is part of the Hanzo ComfyUI ecosystem, ensuring tested compatibility and seamless integration with other Hanzo custom nodes.
|
||||
|
||||
## Video Formats
|
||||
Those familiar with ffmpeg are able to add json files to the video_formats folders to add new output types to Video Combine.
|
||||
Consider the following example for av1-webm
|
||||
```json
|
||||
{
|
||||
"main_pass":
|
||||
[
|
||||
"-n", "-c:v", "libsvtav1",
|
||||
"-pix_fmt", "yuv420p10le",
|
||||
"-crf", ["crf","INT", {"default": 23, "min": 0, "max": 100, "step": 1}]
|
||||
],
|
||||
"audio_pass": ["-c:a", "libopus"],
|
||||
"extension": "webm",
|
||||
"environment": {"SVT_LOG": "1"}
|
||||
}
|
||||
## Installation
|
||||
|
||||
### As Part of Hanzo Painter (Recommended)
|
||||
|
||||
```bash
|
||||
git clone git@github.com:hanzoai/painter.git
|
||||
cd painter
|
||||
make setup # Automatically installs all Hanzo nodes
|
||||
```
|
||||
Most configuration takes place in `main_pass`, which is a list of arguments that are passed to ffmpeg.
|
||||
- `"-n"` designates that the command should fail if a file of the same name already exists. This should never happen, but if some bug were to occur, it would ensure other files aren't overwritten.
|
||||
- `"-c:v", "libsvtav1"` designates that the video should be encoded with an av1 codec using the new SVT-AV1 encoder. SVT-AV1 is much faster than libaom-av1, but may not exist in older versions of ffmpeg. Alternatively, av1_nvenc could be used for gpu encoding with newer nvidia cards.
|
||||
- `"-pix_fmt", "yuv420p10le"` designates the standard pixel format with 10-bit color. It's important that some pixel format be specified to ensure a nonconfigurable input pix_fmt isn't used.
|
||||
|
||||
`audio pass` contains a list of arguments which are passed to ffmpeg when audio is passed into Video Combine
|
||||
### Standalone Installation
|
||||
|
||||
`extension` designates both the file extension and the container format that is used. If some of the above options are omitted from `main_pass` it can affect what default options are chosen.
|
||||
`environment` can optionally be provided to set environment variables during execution. For av1 it's used to reduce the verbosity of logging so that only major errors are displayed.
|
||||
`input_color_depth` effects the format in which pixels are passed to the ffmpeg subprocess. Current valid options are `8bit` and `16bit`. The later will produce higher quality output, but is experimental.
|
||||
```bash
|
||||
cd ComfyUI/custom_nodes
|
||||
git clone git@github.com:hanzoai/Hanzo-VideoHelper.git
|
||||
cd Hanzo-VideoHelper
|
||||
pip install -r requirements.txt # If requirements.txt exists
|
||||
```
|
||||
|
||||
Fields can be exposed in the webui as a widget using a format similar to what is used in the creation of custom nodes. In the above example, the argument for `-crf` will be exposed as a format widget in the webui. Format widgets are a list of up to 3 terms
|
||||
- The name of the widget that will be displayed in the web ui
|
||||
- Either a primitive such as "INT" or "BOOLEAN", or a list of string options
|
||||
- A dictionary of options
|
||||
## Hanzo ComfyUI Ecosystem
|
||||
|
||||
Hanzo-VideoHelper is part of a curated stack of ComfyUI nodes maintained by Hanzo AI. All nodes work together seamlessly and are tested as a unified stack.
|
||||
|
||||
| Node | Purpose |
|
||||
|------|---------|
|
||||
| [Hanzo-DiffuEraser](https://github.com/hanzoai/Hanzo-DiffuEraser) | Content-aware inpainting |
|
||||
| [Hanzo-VideoHelper](https://github.com/hanzoai/Hanzo-VideoHelper) | Video I/O suite |
|
||||
| [Hanzo-EasyUse](https://github.com/hanzoai/Hanzo-EasyUse) | Workflow utilities |
|
||||
| [Hanzo-KJNodes](https://github.com/hanzoai/Hanzo-KJNodes) | Core utilities |
|
||||
| [Hanzo-LayerStyle](https://github.com/hanzoai/Hanzo-LayerStyle) | Layer compositing |
|
||||
| [Hanzo-MLX](https://github.com/hanzoai/Hanzo-MLX) | Apple Silicon acceleration |
|
||||
|
||||
## Upstream
|
||||
|
||||
This is a Hanzo-maintained fork of [Kosinkadink/ComfyUI-VideoHelperSuite](https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite).
|
||||
|
||||
- **Upstream Sync**: Regularly updated from original repository
|
||||
- **Hanzo Enhancements**: Custom improvements for Hanzo ecosystem
|
||||
- **Tested Compatibility**: Verified to work with other Hanzo nodes
|
||||
|
||||
## Contributing
|
||||
|
||||
### To Hanzo Fork
|
||||
For Hanzo-specific features:
|
||||
1. Fork this repository
|
||||
2. Create a feature branch
|
||||
3. Submit a PR to `hanzoai/Hanzo-VideoHelper`
|
||||
|
||||
### To Upstream
|
||||
For general improvements:
|
||||
1. Submit PRs to [Kosinkadink/ComfyUI-VideoHelperSuite](https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite)
|
||||
2. Hanzo fork will sync these improvements
|
||||
|
||||
## About Hanzo AI
|
||||
|
||||
Part of the [Hanzo AI](https://hanzo.ai) ecosystem:
|
||||
|
||||
- **hanzo.ai** - Core AI infrastructure platform
|
||||
- **hanzo.io** - Business solutions
|
||||
- **hanzo.network** - Decentralized compute marketplace
|
||||
|
||||
---
|
||||
|
||||
Made with ❤️ by [Hanzo AI](https://hanzo.ai)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from .videohelpersuite.nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
import folder_paths
|
||||
from .videohelpersuite.server import server
|
||||
from .videohelpersuite import documentation
|
||||
from .videohelpersuite import latent_preview
|
||||
|
||||
WEB_DIRECTORY = "./web"
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
documentation.format_descriptions(NODE_CLASS_MAPPINGS)
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
[project]
|
||||
name = "comfyui-videohelpersuite"
|
||||
description = "Nodes related to video workflows"
|
||||
version = "1.2.0"
|
||||
license = "LICENSE"
|
||||
version = "1.7.9"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["opencv-python", "imageio-ffmpeg"]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
Code to automate execution of the tests and evaluate the results.
|
||||
Distributed as a `custom node`, and can be installed by copying or simlinking to the `custom_nodes` directory.
|
||||
Requires that ffprobe be available and added to the path. Note that imageio-ffmpeg does not bundle ffprobe.
|
||||
|
||||
When installed, it adds a new sidebar tab to automate running one, or a folder of tests. This requires that the `Use new menu and workflow management` setting not be disabled
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
from . import server
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
|
||||
WEB_DIRECTORY = "./web"
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
@@ -0,0 +1,60 @@
|
||||
import subprocess
|
||||
import json
|
||||
import os
|
||||
import torch
|
||||
import shutil
|
||||
|
||||
import server
|
||||
import folder_paths
|
||||
|
||||
web = server.web
|
||||
|
||||
@server.PromptServer.instance.routes.post("/VHS_test")
|
||||
async def test(request):
|
||||
try:
|
||||
req_data = await request.json()
|
||||
output = req_data['output']['gifs'][0]
|
||||
filename = output['filename']
|
||||
typ = output['type']
|
||||
base_args = ["ffprobe", "-v", "error", '-count_packets', "-show_entries", "stream", "-of", "json"]
|
||||
video = folder_paths.get_annotated_filepath(f'{filename} [{typ}]')
|
||||
vprobe = json.loads(subprocess.run(base_args + ['-select_streams', 'v:0', video],
|
||||
capture_output=True, check=True).stdout)['streams'][0]
|
||||
aprobe = json.loads(subprocess.run(base_args + ['-select_streams', 'a:0', video],
|
||||
capture_output=True, check=True).stdout)['streams']
|
||||
probe = {'video': vprobe}
|
||||
if len(aprobe) > 0:
|
||||
probe['audio'] = aprobe[0]
|
||||
errors = []
|
||||
compare = None
|
||||
for test in req_data['tests']:
|
||||
if test['type'] == 'compare':
|
||||
compare = test
|
||||
continue
|
||||
key = test['key']
|
||||
expected = test['value']
|
||||
actual = probe[test['type']][key]
|
||||
if expected != actual:
|
||||
#Consider always dumping type?
|
||||
errors.append(f'{key}: {expected} != {actual}')
|
||||
if len(errors) == 0 and compare is not None:
|
||||
if not os.path.exists(compare['filename']):
|
||||
os.makedirs(os.path.split(compare['filename'])[0], exist_ok=True)
|
||||
shutil.copy(video, compare['filename'])
|
||||
print("Missing comparison file has been initialized from output:", os.path.abspath(compare['filename']))
|
||||
else:
|
||||
#NOTE: This does not include the full memory optimizations of VHS
|
||||
#Tests should be small
|
||||
#TODO: Figure out way to do opacity comparison. May need to do blending in python
|
||||
#(easy, but slower and more memory intensive)
|
||||
diff = subprocess.run(['ffmpeg', '-v', 'error', '-i', video, '-i', compare['filename'], '-filter_complex', 'blend=all_mode=grainextract', '-pix_fmt', 'rgb24', '-f', 'rawvideo', '-'], stdout=subprocess.PIPE, check=True).stdout
|
||||
diff = torch.frombuffer(diff, dtype=torch.uint8).to(dtype=torch.float32).div_(255)
|
||||
#diff = diff.reshape((-1,4))
|
||||
d = (diff-0.5).abs().sum()/diff.size(0)
|
||||
if d > compare['tolerance']:
|
||||
errors.append(f'Similarity is outside specified tolerance: {d}')
|
||||
else:
|
||||
print('d:', d)
|
||||
return web.json_response(errors)
|
||||
except Exception as e:
|
||||
return web.json_response(str(e))
|
||||
@@ -0,0 +1,96 @@
|
||||
|
||||
import {app} from "../../../scripts/app.js";
|
||||
import {api} from "../../../scripts/api.js";
|
||||
|
||||
let watched_nodes = {}
|
||||
let resolve = undefined
|
||||
let testURL = api.apiURL("/VHS_test")
|
||||
let errors = []
|
||||
api.addEventListener("executed", async function ({detail}) {
|
||||
if (watched_nodes && watched_nodes[detail?.node]) {
|
||||
if (detail?.output?.unfinished_batch) {
|
||||
return
|
||||
}
|
||||
let requestBody = {tests: watched_nodes[detail.node], output: detail.output}
|
||||
try {
|
||||
let req = await fetch(api.apiURL("/VHS_test"),
|
||||
{method: "POST", body: JSON.stringify(requestBody)});
|
||||
let testResult = await req.json()
|
||||
if (testResult.length != 0) {
|
||||
errors.push(testResult)
|
||||
}
|
||||
} catch(e) {
|
||||
errors.push(e)
|
||||
}
|
||||
if (!(watched_nodes.length -= 1)) {
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const workflowService = app.extensionManager.workflow
|
||||
|
||||
async function runTest(file) {
|
||||
if (!file?.name?.endsWith(".json")) {
|
||||
return false
|
||||
}
|
||||
let workflow = JSON.parse(await file.text())
|
||||
await app.loadGraphData(workflow)
|
||||
//NOTE: API is not used so workflow data is actually processed
|
||||
watched_nodes = workflow.tests
|
||||
errors = []
|
||||
let p = new Promise((r) => resolve = r)
|
||||
await app.queuePrompt()
|
||||
//block until execution completes
|
||||
await p
|
||||
watched_nodes = {}
|
||||
if (errors.length > 0) {
|
||||
app.ui.dialog.show("Failed " + errors.length + " tests:\n" + errors)
|
||||
return true
|
||||
}
|
||||
await workflowService.closeWorkflow(workflowService.activeWorkflow, {warnIfUnsaved: false})
|
||||
return false
|
||||
}
|
||||
let iconOverride = document.createElement("style")
|
||||
iconOverride.innerHTML = `.VHSTestIcon:before {content: '🧪';}`
|
||||
document.body.append(iconOverride)
|
||||
|
||||
let testSidebar = {id: 'VHStest', title: 'VHS Test', icon: 'VHSTestIcon', type: 'custom',
|
||||
render: (e) => {
|
||||
e.innerHTML = `Select a folder containing tests
|
||||
<input>
|
||||
Or select a single test
|
||||
<input>
|
||||
`
|
||||
|
||||
const folderInput = e.children[0]
|
||||
const fileInput = e.children[1]
|
||||
Object.assign(folderInput, {
|
||||
type: "file",
|
||||
webkitdirectory: true,
|
||||
onchange: async function() {
|
||||
const startTime = Date.now()
|
||||
let failedTests = false
|
||||
for(const file of this.files) {
|
||||
failedTests ||= await runTest(file)
|
||||
}
|
||||
this.value=""
|
||||
if (!failedTests) {
|
||||
console.log("All tests passed in " + ((Date.now() - startTime)/1000) + "s")
|
||||
}
|
||||
},
|
||||
});
|
||||
Object.assign(fileInput, {
|
||||
type: "file",
|
||||
accept: ".json",
|
||||
onchange: async function() {
|
||||
if (this.files.length) {
|
||||
if(!(await runTest(this.files[0]))) {
|
||||
console.log("Test complete")
|
||||
}
|
||||
this.value=""
|
||||
}
|
||||
},
|
||||
});
|
||||
}}
|
||||
app.extensionManager.registerSidebarTab(testSidebar)
|
||||
@@ -0,0 +1 @@
|
||||
Workflows for automated testing of VHS. Most include an additional tests key to check the properties or perform comparisons on node outputs
|
||||
@@ -0,0 +1,313 @@
|
||||
{
|
||||
"id": "07b812b5-5037-4878-90bc-32d3a1f36619",
|
||||
"revision": 0,
|
||||
"last_node_id": 7,
|
||||
"last_link_id": 5,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 5,
|
||||
"type": "VHS_VideoCombine",
|
||||
"pos": [
|
||||
732,
|
||||
-23
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
334
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 2
|
||||
},
|
||||
{
|
||||
"name": "audio",
|
||||
"shape": 7,
|
||||
"type": "AUDIO",
|
||||
"link": 3
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
"Node name for S&R": "VHS_VideoCombine"
|
||||
},
|
||||
"widgets_values": {
|
||||
"frame_rate": 8,
|
||||
"loop_count": 0,
|
||||
"filename_prefix": "AnimateDiff",
|
||||
"format": "video/webm",
|
||||
"pix_fmt": "yuv420p",
|
||||
"crf": 20,
|
||||
"save_metadata": true,
|
||||
"trim_to_audio": false,
|
||||
"pingpong": false,
|
||||
"save_output": false,
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false,
|
||||
"params": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "VHS_VideoCombine",
|
||||
"pos": [
|
||||
503,
|
||||
363
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
334
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 4
|
||||
},
|
||||
{
|
||||
"name": "audio",
|
||||
"shape": 7,
|
||||
"type": "AUDIO",
|
||||
"link": 5
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
"Node name for S&R": "VHS_VideoCombine"
|
||||
},
|
||||
"widgets_values": {
|
||||
"frame_rate": 8,
|
||||
"loop_count": 0,
|
||||
"filename_prefix": "AnimateDiff",
|
||||
"format": "video/h264-mp4",
|
||||
"pix_fmt": "yuv420p",
|
||||
"crf": 19,
|
||||
"save_metadata": true,
|
||||
"trim_to_audio": false,
|
||||
"pingpong": false,
|
||||
"save_output": false,
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false,
|
||||
"params": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "VHS_LoadVideoPath",
|
||||
"pos": [
|
||||
29,
|
||||
16
|
||||
],
|
||||
"size": [
|
||||
221.27618408203125,
|
||||
413.1552734375
|
||||
],
|
||||
"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": [
|
||||
2,
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "frame_count",
|
||||
"type": "INT",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "audio",
|
||||
"type": "AUDIO",
|
||||
"links": [
|
||||
3
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "video_info",
|
||||
"type": "VHS_VIDEOINFO",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VHS_LoadVideoPath"
|
||||
},
|
||||
"widgets_values": {
|
||||
"video": "input/bigbuckbunny.mp4",
|
||||
"force_rate": 8,
|
||||
"custom_width": 0,
|
||||
"custom_height": 0,
|
||||
"frame_load_cap": 30,
|
||||
"skip_first_frames": 0,
|
||||
"select_every_nth": 1,
|
||||
"format": "AnimateDiff",
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false,
|
||||
"params": {
|
||||
"filename": "input/bigbuckbunny.mp4",
|
||||
"type": "path",
|
||||
"format": "video/mp4",
|
||||
"force_rate": 8,
|
||||
"custom_width": 0,
|
||||
"custom_height": 0,
|
||||
"frame_load_cap": 30,
|
||||
"skip_first_frames": 0,
|
||||
"select_every_nth": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "VHS_LoadAudio",
|
||||
"pos": [
|
||||
83,
|
||||
564
|
||||
],
|
||||
"size": [
|
||||
218.93820190429688,
|
||||
126
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "audio",
|
||||
"type": "AUDIO",
|
||||
"links": [
|
||||
5
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "duration",
|
||||
"type": "FLOAT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VHS_LoadAudio"
|
||||
},
|
||||
"widgets_values": {
|
||||
"audio_file": "input/bigbuckbunny.mp4",
|
||||
"seek_seconds": 0,
|
||||
"duration": 5
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
2,
|
||||
4,
|
||||
0,
|
||||
5,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
3,
|
||||
4,
|
||||
2,
|
||||
5,
|
||||
1,
|
||||
"AUDIO"
|
||||
],
|
||||
[
|
||||
4,
|
||||
4,
|
||||
0,
|
||||
6,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
5,
|
||||
7,
|
||||
0,
|
||||
6,
|
||||
1,
|
||||
"AUDIO"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"frontendVersion": "1.25.0",
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4,
|
||||
"tests": {
|
||||
"5": [{"type": "audio", "key": "nb_read_packets", "value": "177"}],
|
||||
"6": [{"type": "audio", "key": "nb_read_packets", "value": "176"}],
|
||||
"length": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,535 @@
|
||||
{
|
||||
"last_node_id": 18,
|
||||
"last_link_id": 28,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 16,
|
||||
"type": "SolidMask",
|
||||
"pos": [
|
||||
38,
|
||||
1066
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 106
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": [
|
||||
18
|
||||
],
|
||||
"slot_index": 0,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SolidMask"
|
||||
},
|
||||
"widgets_values": [
|
||||
1,
|
||||
512,
|
||||
512
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"type": "ImageCompositeMasked",
|
||||
"pos": [
|
||||
412.0800030517579,
|
||||
591.4099975585939
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 146
|
||||
},
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "destination",
|
||||
"type": "IMAGE",
|
||||
"link": 16
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"type": "IMAGE",
|
||||
"link": 17
|
||||
},
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": 18
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
19
|
||||
],
|
||||
"slot_index": 0,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ImageCompositeMasked"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
0,
|
||||
false
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"type": "VHS_BatchManager",
|
||||
"pos": [
|
||||
329.8000881958008,
|
||||
-87.40008056640622
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 58
|
||||
},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "VHS_BatchManager",
|
||||
"type": "VHS_BatchManager",
|
||||
"links": [
|
||||
24,
|
||||
25,
|
||||
26,
|
||||
27
|
||||
],
|
||||
"slot_index": 0,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VHS_BatchManager"
|
||||
},
|
||||
"widgets_values": {
|
||||
"frames_per_batch": 4,
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "VHS_LoadVideoPath",
|
||||
"pos": [
|
||||
24,
|
||||
102
|
||||
],
|
||||
"size": [
|
||||
320,
|
||||
420.7188019966722
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "meta_batch",
|
||||
"type": "VHS_BatchManager",
|
||||
"link": 25,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
16,
|
||||
22
|
||||
],
|
||||
"slot_index": 0,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "frame_count",
|
||||
"type": "INT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "audio",
|
||||
"type": "AUDIO",
|
||||
"links": [
|
||||
28
|
||||
],
|
||||
"slot_index": 2,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "video_info",
|
||||
"type": "VHS_VIDEOINFO",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VHS_LoadVideoPath"
|
||||
},
|
||||
"widgets_values": {
|
||||
"video": "input/bigbuckbunny.mp4",
|
||||
"force_rate": 8,
|
||||
"force_size": "512x?",
|
||||
"custom_width": 512,
|
||||
"custom_height": 512,
|
||||
"frame_load_cap": 16,
|
||||
"skip_first_frames": 64,
|
||||
"select_every_nth": 1,
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false,
|
||||
"params": {
|
||||
"frame_load_cap": 16,
|
||||
"skip_first_frames": 64,
|
||||
"force_rate": 8,
|
||||
"filename": "input/bigbuckbunny.mp4",
|
||||
"type": "path",
|
||||
"format": "video/mp4",
|
||||
"select_every_nth": 1,
|
||||
"force_size": "512x?"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"type": "VHS_LoadVideoPath",
|
||||
"pos": [
|
||||
26,
|
||||
598
|
||||
],
|
||||
"size": [
|
||||
320,
|
||||
444.7188019966722
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "meta_batch",
|
||||
"type": "VHS_BatchManager",
|
||||
"link": 26,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
17
|
||||
],
|
||||
"slot_index": 0,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "frame_count",
|
||||
"type": "INT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "audio",
|
||||
"type": "VHS_AUDIO",
|
||||
"links": [],
|
||||
"slot_index": 2,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "video_info",
|
||||
"type": "VHS_VIDEOINFO",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VHS_LoadVideoPath"
|
||||
},
|
||||
"widgets_values": {
|
||||
"video": "input/bigbuckbunny.mp4",
|
||||
"force_rate": 8,
|
||||
"force_size": "Custom Width",
|
||||
"custom_width": 384,
|
||||
"custom_height": 512,
|
||||
"frame_load_cap": 16,
|
||||
"skip_first_frames": 64,
|
||||
"select_every_nth": 1,
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false,
|
||||
"params": {
|
||||
"frame_load_cap": 16,
|
||||
"skip_first_frames": 64,
|
||||
"force_rate": 8,
|
||||
"filename": "input/bigbuckbunny.mp4",
|
||||
"type": "path",
|
||||
"format": "video/mp4",
|
||||
"select_every_nth": 1,
|
||||
"force_size": "512x?"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "VHS_VideoCombine",
|
||||
"pos": [
|
||||
762,
|
||||
646
|
||||
],
|
||||
"size": [
|
||||
320,
|
||||
492.75
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 19
|
||||
},
|
||||
{
|
||||
"name": "audio",
|
||||
"type": "AUDIO",
|
||||
"link": 28
|
||||
},
|
||||
{
|
||||
"name": "meta_batch",
|
||||
"type": "VHS_BatchManager",
|
||||
"link": 27,
|
||||
"slot_index": 2
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "Filenames",
|
||||
"type": "VHS_FILENAMES",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VHS_VideoCombine"
|
||||
},
|
||||
"widgets_values": {
|
||||
"frame_rate": 8,
|
||||
"loop_count": 0,
|
||||
"filename_prefix": "AnimateDiff",
|
||||
"format": "video/h264-mp4",
|
||||
"pix_fmt": "yuv420p",
|
||||
"crf": 19,
|
||||
"save_metadata": true,
|
||||
"pingpong": false,
|
||||
"save_output": false,
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false,
|
||||
"params": {
|
||||
"filename": "AnimateDiff_00005-audio.mp4",
|
||||
"subfolder": "",
|
||||
"type": "temp",
|
||||
"format": "video/h264-mp4",
|
||||
"frame_rate": 8
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"type": "VHS_VideoCombine",
|
||||
"pos": [
|
||||
756,
|
||||
100
|
||||
],
|
||||
"size": [
|
||||
320,
|
||||
492.75
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 22
|
||||
},
|
||||
{
|
||||
"name": "audio",
|
||||
"type": "VHS_AUDIO",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "meta_batch",
|
||||
"type": "VHS_BatchManager",
|
||||
"link": 24,
|
||||
"slot_index": 2
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "Filenames",
|
||||
"type": "VHS_FILENAMES",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VHS_VideoCombine"
|
||||
},
|
||||
"widgets_values": {
|
||||
"frame_rate": 8,
|
||||
"loop_count": 0,
|
||||
"filename_prefix": "AnimateDiff",
|
||||
"format": "video/webm",
|
||||
"pix_fmt": "yuv420p",
|
||||
"crf": 20,
|
||||
"save_metadata": true,
|
||||
"pingpong": false,
|
||||
"save_output": false,
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false,
|
||||
"params": {
|
||||
"filename": "AnimateDiff_00004.webm",
|
||||
"subfolder": "",
|
||||
"type": "temp",
|
||||
"format": "video/webm",
|
||||
"frame_rate": 8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
16,
|
||||
10,
|
||||
0,
|
||||
15,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
17,
|
||||
14,
|
||||
0,
|
||||
15,
|
||||
1,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
18,
|
||||
16,
|
||||
0,
|
||||
15,
|
||||
2,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
19,
|
||||
15,
|
||||
0,
|
||||
11,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
22,
|
||||
10,
|
||||
0,
|
||||
17,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
24,
|
||||
18,
|
||||
0,
|
||||
17,
|
||||
2,
|
||||
"VHS_BatchManager"
|
||||
],
|
||||
[
|
||||
25,
|
||||
18,
|
||||
0,
|
||||
10,
|
||||
0,
|
||||
"VHS_BatchManager"
|
||||
],
|
||||
[
|
||||
26,
|
||||
18,
|
||||
0,
|
||||
14,
|
||||
0,
|
||||
"VHS_BatchManager"
|
||||
],
|
||||
[
|
||||
27,
|
||||
18,
|
||||
0,
|
||||
11,
|
||||
2,
|
||||
"VHS_BatchManager"
|
||||
],
|
||||
[
|
||||
28,
|
||||
10,
|
||||
2,
|
||||
11,
|
||||
1,
|
||||
"VHS_AUDIO"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"version": 0.4,
|
||||
"tests": {
|
||||
"17": [{"type": "video", "key": "nb_read_packets", "value": "16"}],
|
||||
"11": [{"type": "video", "key": "nb_read_packets", "value": "16"},
|
||||
{"type": "compare", "filename": "custom_nodes/ComfyUI-VideoHelperSuite/tests/outputs/batch.mp4", "tolerance": 0.02}
|
||||
],
|
||||
"length": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
{
|
||||
"last_node_id": 19,
|
||||
"last_link_id": 20,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 18,
|
||||
"type": "PrimitiveNode",
|
||||
"pos": [
|
||||
318,
|
||||
618
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
82
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "INT",
|
||||
"type": "INT",
|
||||
"links": [
|
||||
18
|
||||
],
|
||||
"widget": {
|
||||
"name": "crf"
|
||||
}
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Run widget replace on values": false
|
||||
},
|
||||
"widgets_values": [
|
||||
60,
|
||||
"fixed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
260.4530029296875,
|
||||
233.2003173828125
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
19
|
||||
],
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"example.png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"type": "VHS_VideoCombine",
|
||||
"pos": [
|
||||
733.3749389648438,
|
||||
338.28924560546875
|
||||
],
|
||||
"size": [
|
||||
222.91415405273438,
|
||||
522.9141845703125
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 19
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
{
|
||||
"name": "crf",
|
||||
"type": "INT",
|
||||
"link": 18,
|
||||
"widget": {
|
||||
"name": "crf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "pix_fmt",
|
||||
"type": [
|
||||
"yuv420p",
|
||||
"yuv420p10le"
|
||||
],
|
||||
"link": 20,
|
||||
"widget": {
|
||||
"name": "pix_fmt"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "Filenames",
|
||||
"type": "VHS_FILENAMES",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VHS_VideoCombine"
|
||||
},
|
||||
"widgets_values": {
|
||||
"frame_rate": 8,
|
||||
"loop_count": 0,
|
||||
"filename_prefix": "AnimateDiff",
|
||||
"format": "video/h264-mp4",
|
||||
"pix_fmt": "yuv420p10le",
|
||||
"crf": 60,
|
||||
"save_metadata": true,
|
||||
"pingpong": false,
|
||||
"save_output": false,
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false,
|
||||
"params": {
|
||||
"filename": "AnimateDiff_00001.mp4",
|
||||
"subfolder": "",
|
||||
"type": "temp",
|
||||
"format": "video/h264-mp4",
|
||||
"frame_rate": 8
|
||||
},
|
||||
"muted": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"type": "PrimitiveNode",
|
||||
"pos": [
|
||||
300,
|
||||
760
|
||||
],
|
||||
"size": [
|
||||
290,
|
||||
110
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "COMBO",
|
||||
"type": "COMBO",
|
||||
"links": [
|
||||
20
|
||||
],
|
||||
"widget": {
|
||||
"name": "pix_fmt"
|
||||
}
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Run widget replace on values": false
|
||||
},
|
||||
"widgets_values": [
|
||||
"yuv420p10le",
|
||||
"fixed",
|
||||
""
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
18,
|
||||
18,
|
||||
0,
|
||||
17,
|
||||
4,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
19,
|
||||
11,
|
||||
0,
|
||||
17,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
20,
|
||||
19,
|
||||
0,
|
||||
17,
|
||||
5,
|
||||
[
|
||||
"yuv420p",
|
||||
"yuv420p10le"
|
||||
]
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.8264462809917354,
|
||||
"offset": [
|
||||
45.8650452880864,
|
||||
-157.46987175292935
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4,
|
||||
"tests": {
|
||||
"17": [{"type": "video", "key": "pix_fmt", "value": "yuv420p10le"}
|
||||
],
|
||||
"length": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
{
|
||||
"last_node_id": 20,
|
||||
"last_link_id": 23,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 18,
|
||||
"type": "VHS_VideoInfoLoaded",
|
||||
"pos": [
|
||||
424,
|
||||
427
|
||||
],
|
||||
"size": {
|
||||
"0": 304.79998779296875,
|
||||
"1": 106
|
||||
},
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "video_info",
|
||||
"type": "VHS_VIDEOINFO",
|
||||
"link": 19
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "fps🟦",
|
||||
"type": "FLOAT",
|
||||
"links": [
|
||||
20
|
||||
],
|
||||
"slot_index": 0,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "frame_count🟦",
|
||||
"type": "INT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "duration🟦",
|
||||
"type": "FLOAT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "width🟦",
|
||||
"type": "INT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "height🟦",
|
||||
"type": "INT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VHS_VideoInfoLoaded"
|
||||
},
|
||||
"widgets_values": {}
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"type": "VHS_VideoCombine",
|
||||
"pos": [
|
||||
783,
|
||||
223
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
286
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 21
|
||||
},
|
||||
{
|
||||
"name": "audio",
|
||||
"type": "VHS_AUDIO",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "meta_batch",
|
||||
"type": "VHS_BatchManager",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "frame_rate",
|
||||
"type": "FLOAT",
|
||||
"link": 20,
|
||||
"widget": {
|
||||
"name": "frame_rate"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "Filenames",
|
||||
"type": "VHS_FILENAMES",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VHS_VideoCombine"
|
||||
},
|
||||
"widgets_values": {
|
||||
"frame_rate": 8,
|
||||
"loop_count": 0,
|
||||
"filename_prefix": "AnimateDiff",
|
||||
"format": "video/h264-mp4",
|
||||
"pix_fmt": "yuv420p",
|
||||
"crf": 19,
|
||||
"save_metadata": true,
|
||||
"pingpong": false,
|
||||
"save_output": false,
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false,
|
||||
"params": {
|
||||
"filename": "AnimateDiff_00001.mp4",
|
||||
"subfolder": "",
|
||||
"type": "temp",
|
||||
"format": "video/h264-mp4"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"type": "VHS_LoadVideoPath",
|
||||
"pos": [
|
||||
96,
|
||||
230
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
449.5
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "meta_batch",
|
||||
"type": "VHS_BatchManager",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "frame_load_cap",
|
||||
"type": "INT",
|
||||
"link": 23,
|
||||
"slot_index": 1,
|
||||
"widget": {
|
||||
"name": "frame_load_cap"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
21
|
||||
],
|
||||
"slot_index": 0,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "frame_count",
|
||||
"type": "INT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "audio",
|
||||
"type": "VHS_AUDIO",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "video_info",
|
||||
"type": "VHS_VIDEOINFO",
|
||||
"links": [
|
||||
19
|
||||
],
|
||||
"slot_index": 3,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VHS_LoadVideoPath"
|
||||
},
|
||||
"widgets_values": {
|
||||
"video": "input/leader.webm",
|
||||
"force_rate": 0,
|
||||
"force_size": "Disabled",
|
||||
"custom_width": 512,
|
||||
"custom_height": 512,
|
||||
"frame_load_cap": 64,
|
||||
"skip_first_frames": 0,
|
||||
"select_every_nth": 1,
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false,
|
||||
"params": {
|
||||
"frame_load_cap": 64,
|
||||
"skip_first_frames": 0,
|
||||
"force_rate": 0,
|
||||
"filename": "input/leader.webm",
|
||||
"type": "path",
|
||||
"format": "video/webm",
|
||||
"select_every_nth": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"type": "PrimitiveNode",
|
||||
"pos": [
|
||||
-240,
|
||||
170
|
||||
],
|
||||
"size": {
|
||||
"0": 210,
|
||||
"1": 80
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "INT",
|
||||
"type": "INT",
|
||||
"links": [
|
||||
23
|
||||
],
|
||||
"widget": {
|
||||
"name": "frame_load_cap"
|
||||
}
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Run widget replace on values": false
|
||||
},
|
||||
"widgets_values": [
|
||||
64,
|
||||
"fixed"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
19,
|
||||
16,
|
||||
3,
|
||||
18,
|
||||
0,
|
||||
"VHS_VIDEOINFO"
|
||||
],
|
||||
[
|
||||
20,
|
||||
18,
|
||||
0,
|
||||
17,
|
||||
3,
|
||||
"FLOAT"
|
||||
],
|
||||
[
|
||||
21,
|
||||
16,
|
||||
0,
|
||||
17,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
23,
|
||||
20,
|
||||
0,
|
||||
16,
|
||||
1,
|
||||
"INT"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"version": 0.4,
|
||||
"tests": {
|
||||
"17": [{"type": "video", "key": "width", "value": 1440},
|
||||
{"type": "video", "key": "height", "value": 1080},
|
||||
{"type": "video", "key": "nb_read_packets", "value": "64"}
|
||||
],
|
||||
"length": 1
|
||||
}
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
{
|
||||
"last_node_id": 3,
|
||||
"last_link_id": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "VHS_LoadVideo",
|
||||
"pos": {
|
||||
"0": 54,
|
||||
"1": 89
|
||||
},
|
||||
"size": [
|
||||
260,
|
||||
460
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "meta_batch",
|
||||
"type": "VHS_BatchManager",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
1
|
||||
],
|
||||
"slot_index": 0,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "frame_count",
|
||||
"type": "INT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "audio",
|
||||
"type": "VHS_AUDIO",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "video_info",
|
||||
"type": "VHS_VIDEOINFO",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VHS_LoadVideo"
|
||||
},
|
||||
"widgets_values": {
|
||||
"video": "leader.webm",
|
||||
"force_rate": 8,
|
||||
"force_size": "Disabled",
|
||||
"custom_width": 304,
|
||||
"custom_height": 312,
|
||||
"frame_load_cap": 16,
|
||||
"skip_first_frames": 1,
|
||||
"select_every_nth": 1,
|
||||
"choose video to upload": "image",
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false,
|
||||
"params": {
|
||||
"frame_load_cap": 16,
|
||||
"skip_first_frames": 1,
|
||||
"force_rate": 8,
|
||||
"filename": "leader.webm",
|
||||
"type": "input",
|
||||
"format": "video/mp4",
|
||||
"force_size": "410.4x?",
|
||||
"select_every_nth": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "VHS_VideoCombine",
|
||||
"pos": {
|
||||
"0": 629,
|
||||
"1": 222
|
||||
},
|
||||
"size": [
|
||||
320,
|
||||
550
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 1
|
||||
},
|
||||
{
|
||||
"name": "audio",
|
||||
"type": "AUDIO",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "meta_batch",
|
||||
"type": "VHS_BatchManager",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "Filenames",
|
||||
"type": "VHS_FILENAMES",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VHS_VideoCombine"
|
||||
},
|
||||
"widgets_values": {
|
||||
"frame_rate": 8,
|
||||
"loop_count": 1,
|
||||
"filename_prefix": "AnimateDiff",
|
||||
"format": "video/h264-mp4",
|
||||
"pix_fmt": "yuv420p",
|
||||
"crf": 19,
|
||||
"save_metadata": true,
|
||||
"pingpong": false,
|
||||
"save_output": false,
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false,
|
||||
"params": {
|
||||
"filename": "AnimateDiff_00005.mp4",
|
||||
"subfolder": "",
|
||||
"type": "temp",
|
||||
"format": "video/h264-mp4",
|
||||
"frame_rate": 8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"version": 0.4,
|
||||
"tests": {
|
||||
"3": [{"type": "video", "key": "nb_read_packets", "value": "32"}],
|
||||
"length": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
{
|
||||
"id": "bc7448ba-118f-4226-95ab-32227992f954",
|
||||
"revision": 0,
|
||||
"last_node_id": 6,
|
||||
"last_link_id": 4,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "VHS_LoadVideo",
|
||||
"pos": [
|
||||
54,
|
||||
89
|
||||
],
|
||||
"size": [
|
||||
245.1999969482422,
|
||||
492.7751770019531
|
||||
],
|
||||
"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",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": "leader.webm",
|
||||
"force_rate": 8,
|
||||
"custom_width": 512,
|
||||
"custom_height": 0,
|
||||
"frame_load_cap": 64,
|
||||
"skip_first_frames": 1,
|
||||
"select_every_nth": 1,
|
||||
"format": "AnimateDiff",
|
||||
"choose video to upload": "image",
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false,
|
||||
"params": {
|
||||
"frame_load_cap": 64,
|
||||
"skip_first_frames": 0,
|
||||
"force_rate": 8,
|
||||
"filename": "leader.webm",
|
||||
"type": "input",
|
||||
"format": "video/mp4",
|
||||
"select_every_nth": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "VHS_VideoCombine",
|
||||
"pos": [
|
||||
630.9500122070312,
|
||||
136.90997314453125
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
497.25
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 2
|
||||
},
|
||||
{
|
||||
"name": "audio",
|
||||
"shape": 7,
|
||||
"type": "AUDIO",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
"Node name for S&R": "VHS_VideoCombine"
|
||||
},
|
||||
"widgets_values": {
|
||||
"frame_rate": 8,
|
||||
"loop_count": 0,
|
||||
"filename_prefix": "AnimateDiff",
|
||||
"format": "video/ProRes",
|
||||
"profile": "2",
|
||||
"pingpong": false,
|
||||
"save_output": false,
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false,
|
||||
"params": {
|
||||
"filename": "AnimateDiff_00004.mov",
|
||||
"subfolder": "",
|
||||
"type": "temp",
|
||||
"format": "video/ProRes",
|
||||
"frame_rate": 8,
|
||||
"workflow": "AnimateDiff_00004.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "VHS_VideoCombine",
|
||||
"pos": [
|
||||
635.0499267578125,
|
||||
-407.1000061035156
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
497.25
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 1
|
||||
},
|
||||
{
|
||||
"name": "audio",
|
||||
"shape": 7,
|
||||
"type": "AUDIO",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
"Node name for S&R": "VHS_VideoCombine"
|
||||
},
|
||||
"widgets_values": {
|
||||
"frame_rate": 8,
|
||||
"loop_count": 0,
|
||||
"filename_prefix": "AnimateDiff",
|
||||
"format": "video/ProRes",
|
||||
"profile": "1",
|
||||
"pingpong": false,
|
||||
"save_output": false,
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false,
|
||||
"params": {
|
||||
"filename": "AnimateDiff_00001.mov",
|
||||
"subfolder": "",
|
||||
"type": "temp",
|
||||
"format": "video/ProRes",
|
||||
"frame_rate": 8,
|
||||
"workflow": "AnimateDiff_00001.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "VHS_VideoCombine",
|
||||
"pos": [
|
||||
974.6401977539062,
|
||||
-409.33984375
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
497.25
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 3
|
||||
},
|
||||
{
|
||||
"name": "audio",
|
||||
"shape": 7,
|
||||
"type": "AUDIO",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
"Node name for S&R": "VHS_VideoCombine"
|
||||
},
|
||||
"widgets_values": {
|
||||
"frame_rate": 8,
|
||||
"loop_count": 0,
|
||||
"filename_prefix": "AnimateDiff",
|
||||
"format": "video/ProRes",
|
||||
"profile": "3",
|
||||
"pingpong": false,
|
||||
"save_output": false,
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false,
|
||||
"params": {
|
||||
"filename": "AnimateDiff_00002.mov",
|
||||
"subfolder": "",
|
||||
"type": "temp",
|
||||
"format": "video/ProRes",
|
||||
"frame_rate": 8,
|
||||
"workflow": "AnimateDiff_00002.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "VHS_VideoCombine",
|
||||
"pos": [
|
||||
968.7000122070312,
|
||||
138.7698974609375
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
497.25
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 4
|
||||
},
|
||||
{
|
||||
"name": "audio",
|
||||
"shape": 7,
|
||||
"type": "AUDIO",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
"Node name for S&R": "VHS_VideoCombine"
|
||||
},
|
||||
"widgets_values": {
|
||||
"frame_rate": 8,
|
||||
"loop_count": 0,
|
||||
"filename_prefix": "AnimateDiff",
|
||||
"format": "video/ProRes",
|
||||
"profile": "4",
|
||||
"pingpong": false,
|
||||
"save_output": false,
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false,
|
||||
"params": {
|
||||
"filename": "AnimateDiff_00003.mov",
|
||||
"subfolder": "",
|
||||
"type": "temp",
|
||||
"format": "video/ProRes",
|
||||
"frame_rate": 8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
2,
|
||||
1,
|
||||
0,
|
||||
4,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
3,
|
||||
1,
|
||||
0,
|
||||
5,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
4,
|
||||
1,
|
||||
0,
|
||||
6,
|
||||
0,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"frontendVersion": "1.17.0",
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4,
|
||||
"tests": {
|
||||
"6": [{"type": "video", "key": "pix_fmt", "value": "yuv444p12le"}],
|
||||
"length": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
{
|
||||
"last_node_id": 10,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 7,
|
||||
"type": "VHS_VideoCombine",
|
||||
"pos": [
|
||||
746,
|
||||
309
|
||||
],
|
||||
"size": [
|
||||
320,
|
||||
468.7188019966722
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "LATENT",
|
||||
"link": 9
|
||||
},
|
||||
{
|
||||
"name": "audio",
|
||||
"type": "AUDIO",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "meta_batch",
|
||||
"type": "VHS_BatchManager",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 8
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "Filenames",
|
||||
"type": "VHS_FILENAMES",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VHS_VideoCombine"
|
||||
},
|
||||
"widgets_values": {
|
||||
"frame_rate": 8,
|
||||
"loop_count": 0,
|
||||
"filename_prefix": "AnimateDiff",
|
||||
"format": "video/webm",
|
||||
"crf": 20,
|
||||
"save_metadata": true,
|
||||
"pingpong": false,
|
||||
"save_output": false,
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false,
|
||||
"params": {
|
||||
"filename": "AnimateDiff_00001.webm",
|
||||
"subfolder": "",
|
||||
"type": "temp",
|
||||
"format": "video/webm",
|
||||
"frame_rate": 8
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "VHS_LoadVideoPath",
|
||||
"pos": [
|
||||
171,
|
||||
239
|
||||
],
|
||||
"size": [
|
||||
320,
|
||||
420.7188019966722
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "meta_batch",
|
||||
"type": "VHS_BatchManager",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 7,
|
||||
"slot_index": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [
|
||||
9
|
||||
],
|
||||
"slot_index": 0,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "frame_count",
|
||||
"type": "INT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "audio",
|
||||
"type": "AUDIO",
|
||||
"links": [],
|
||||
"slot_index": 2,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "video_info",
|
||||
"type": "VHS_VIDEOINFO",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VHS_LoadVideoPath"
|
||||
},
|
||||
"widgets_values": {
|
||||
"video": "input/bigbuckbunny.mp4",
|
||||
"force_rate": 8,
|
||||
"force_size": "Disabled",
|
||||
"custom_width": 512,
|
||||
"custom_height": 512,
|
||||
"frame_load_cap": 64,
|
||||
"skip_first_frames": 0,
|
||||
"select_every_nth": 1,
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false,
|
||||
"params": {
|
||||
"frame_load_cap": 64,
|
||||
"skip_first_frames": 0,
|
||||
"force_rate": 8,
|
||||
"filename": "input/bigbuckbunny.mp4",
|
||||
"type": "path",
|
||||
"format": "video/mp4",
|
||||
"select_every_nth": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "VAELoader",
|
||||
"pos": [
|
||||
-42,
|
||||
88
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 58
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"links": [
|
||||
7,
|
||||
8
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAELoader"
|
||||
},
|
||||
"widgets_values": [
|
||||
"taesd"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
7,
|
||||
10,
|
||||
0,
|
||||
6,
|
||||
1,
|
||||
"VAE"
|
||||
],
|
||||
[
|
||||
8,
|
||||
10,
|
||||
0,
|
||||
7,
|
||||
3,
|
||||
"VAE"
|
||||
],
|
||||
[
|
||||
9,
|
||||
6,
|
||||
0,
|
||||
7,
|
||||
0,
|
||||
"LATENT"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"version": 0.4,
|
||||
"tests": {
|
||||
"7": [{"type": "video", "key": "width", "value": 1920},
|
||||
{"type": "video", "key": "height", "value": 1080},
|
||||
{"type": "video", "key": "nb_read_packets", "value": "64"}
|
||||
],
|
||||
"length": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"last_node_id": 3,
|
||||
"last_link_id": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "VHS_LoadVideo",
|
||||
"pos": [
|
||||
54,
|
||||
89
|
||||
],
|
||||
"size": [
|
||||
235.1999969482422,
|
||||
384.56999829610186
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
1
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "frame_count",
|
||||
"type": "INT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "audio",
|
||||
"type": "VHS_AUDIO",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VHS_LoadVideo"
|
||||
},
|
||||
"widgets_values": {
|
||||
"video": "leader.webm",
|
||||
"force_rate": 8,
|
||||
"force_size": "Custom Width",
|
||||
"custom_width": 304,
|
||||
"custom_height": 312,
|
||||
"frame_load_cap": 16,
|
||||
"skip_first_frames": 1,
|
||||
"select_every_nth": 1,
|
||||
"choose video to upload": "image",
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false,
|
||||
"params": {
|
||||
"frame_load_cap": 0,
|
||||
"skip_first_frames": 0,
|
||||
"force_rate": 0,
|
||||
"filename": "leader.webm",
|
||||
"type": "input",
|
||||
"format": "video/mp4",
|
||||
"force_size": "410.4x?",
|
||||
"select_every_nth": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "VHS_VideoCombine",
|
||||
"pos": [
|
||||
629,
|
||||
222
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 250
|
||||
},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 1
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "VHS_VideoCombine"
|
||||
},
|
||||
"widgets_values": {
|
||||
"frame_rate": 8,
|
||||
"loop_count": 0,
|
||||
"filename_prefix": "AnimateDiff",
|
||||
"format": "video/webm",
|
||||
"pingpong": false,
|
||||
"save_image": false,
|
||||
"crf": 20,
|
||||
"save_metadata": false,
|
||||
"audio_file": "",
|
||||
"videopreview": {
|
||||
"hidden": false,
|
||||
"paused": false
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"version": 0.4,
|
||||
"tests": {
|
||||
"3": [{"type": "video", "key": "width", "value": 304},
|
||||
{"type": "video", "key": "height", "value": 232},
|
||||
{"type": "compare", "filename": "custom_nodes/ComfyUI-VideoHelperSuite/tests/outputs/simple.webm", "tolerance": 0.02}
|
||||
],
|
||||
"length": 1
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,21 @@
|
||||
"main_pass":
|
||||
[
|
||||
"-n", "-c:v", "prores_ks",
|
||||
"-profile:v","3",
|
||||
"-pix_fmt", "yuv422p10"
|
||||
"-profile:v", [["$profile"]],
|
||||
["profile", {
|
||||
"lt": [[]], "1": [[]], "standard": [[]], "2": [[]], "hq": [[]], "3": [[]],
|
||||
"4": ["has_alpha", {"True": [["-pix_fmt", "yuva444p10le"]],
|
||||
"False": [["-pix_fmt", "yuv444p10le"]]}],
|
||||
"4444": ["has_alpha", {"True": [["-pix_fmt", "yuva444p10le"]],
|
||||
"False": [["-pix_fmt", "yuv444p10le"]]}],
|
||||
"4444xq": ["has_alpha", {"True": [["-pix_fmt", "yuva444p10le"]],
|
||||
"False": [["-pix_fmt", "yuv444p10le"]]}]
|
||||
}],
|
||||
"-vf", "scale=out_color_matrix=bt709",
|
||||
"-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709"
|
||||
],
|
||||
"fake_trc": "bt709",
|
||||
"audio_pass": ["-c:a", "pcm_s16le"],
|
||||
"extension": "mov"
|
||||
"extension": "mov",
|
||||
"extra_widgets": [["profile", ["lt", "standard", "hq", "4444", "4444xq"], {"default": "hq"}]]
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
[
|
||||
"-n", "-c:v", "libsvtav1",
|
||||
"-pix_fmt", ["pix_fmt", ["yuv420p10le", "yuv420p"]],
|
||||
"-crf", ["crf","INT", {"default": 23, "min": 0, "max": 100, "step": 1}]
|
||||
"-crf", ["crf","INT", {"default": 23, "min": 0, "max": 100, "step": 1}],
|
||||
"-vf", "scale=out_color_matrix=bt709",
|
||||
"-color_range", "tv", "-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709"
|
||||
],
|
||||
"fake_trc": "bt709",
|
||||
"audio_pass": ["-c:a", "libopus"],
|
||||
"input_color_depth": ["input_color_depth", ["8bit", "16bit"]],
|
||||
"save_metadata": ["save_metadata", "BOOLEAN", {"default": true}],
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"main_pass": [
|
||||
"-n",
|
||||
"-c:v", "ffv1",
|
||||
"-level", ["level", ["0", "1", "3"], {"default": "3"}],
|
||||
"-coder", ["coder", ["0", "1", "2"], {"default": "1"}],
|
||||
"-context", ["context", ["0", "1"], {"default": "1"}],
|
||||
"-g", ["gop_size", "INT", {"default": 1, "min": 1, "max": 300, "step": 1}],
|
||||
"-slices", ["slices", ["4", "6", "9", "12", "16", "20", "24", "30"], {"default": "16"}],
|
||||
"-slicecrc", ["slicecrc", ["0", "1"], {"default": "1"}],
|
||||
"-pix_fmt", ["pix_fmt", ["rgba64le", "bgra", "yuv420p", "yuv422p", "yuv444p", "yuva420p", "yuva422p", "yuva444p", "yuv420p10le", "yuv422p10le", "yuv444p10le", "yuv420p12le", "yuv422p12le", "yuv444p12le", "yuv420p14le", "yuv422p14le", "yuv444p14le", "yuv420p16le", "yuv422p16le", "yuv444p16le", "gray", "gray10le", "gray12le", "gray16le"], {"default": "rgba64le"}]
|
||||
],
|
||||
"audio_pass": ["-c:a", "flac"],
|
||||
"save_metadata": ["save_metadata", "BOOLEAN", {"default": true}],
|
||||
"trim_to_audio": ["trim_to_audio", "BOOLEAN", {"default": false}],
|
||||
"input_color_depth": "16bit",
|
||||
"extension": "mkv"
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"main_pass":
|
||||
[
|
||||
"-pix_fmt", "yuv420p"
|
||||
"-pix_fmt", "yuv444p",
|
||||
"-vf", "scale=out_color_matrix=bt709:out_range=pc",
|
||||
"-color_range", "pc"
|
||||
],
|
||||
"extension": "gif",
|
||||
"gifski_pass": [
|
||||
|
||||
@@ -3,9 +3,13 @@
|
||||
[
|
||||
"-n", "-c:v", "libx264",
|
||||
"-pix_fmt", ["pix_fmt", ["yuv420p", "yuv420p10le"]],
|
||||
"-crf", ["crf","INT", {"default": 19, "min": 0, "max": 100, "step": 1}]
|
||||
"-crf", ["crf","INT", {"default": 19, "min": 0, "max": 100, "step": 1}],
|
||||
"-vf", "scale=out_color_matrix=bt709",
|
||||
"-color_range", "tv", "-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709"
|
||||
],
|
||||
"audio_pass": ["-c:a", "aac"],
|
||||
"fake_trc": "bt709",
|
||||
"audio_pass": ["-c:a", "aac", "-movflags", "use_metadata_tags"],
|
||||
"save_metadata": ["save_metadata", "BOOLEAN", {"default": true}],
|
||||
"trim_to_audio": ["trim_to_audio", "BOOLEAN", {"default": false}],
|
||||
"extension": "mp4"
|
||||
}
|
||||
|
||||
@@ -6,9 +6,12 @@
|
||||
"-pix_fmt", ["pix_fmt", ["yuv420p10le", "yuv420p"]],
|
||||
"-crf", ["crf","INT", {"default": 22, "min": 0, "max": 100, "step": 1}],
|
||||
"-preset", "medium",
|
||||
"-x265-params", "log-level=quiet"
|
||||
"-x265-params", "log-level=quiet",
|
||||
"-vf", "scale=out_color_matrix=bt709",
|
||||
"-color_range", "tv", "-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709"
|
||||
],
|
||||
"audio_pass": ["-c:a", "aac"],
|
||||
"fake_trc": "bt709",
|
||||
"audio_pass": ["-c:a", "aac", "-movflags", "use_metadata_tags"],
|
||||
"save_metadata": ["save_metadata", "BOOLEAN", {"default": true}],
|
||||
"extension": "mp4"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"main_pass":
|
||||
[
|
||||
"-n", "-c:v", "av1_nvenc",
|
||||
"-pix_fmt", ["pix_fmt", ["yuv420p", "p010le"]],
|
||||
"-vf", "scale=out_color_matrix=bt709",
|
||||
"-color_range", "tv", "-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709"
|
||||
],
|
||||
"fake_trc": "bt709",
|
||||
"audio_pass": ["-c:a", "aac", "-movflags", "use_metadata_tags"],
|
||||
"bitrate": ["bitrate","INT", {"default": 10, "min": 1, "max": 999, "step": 1 }],
|
||||
"megabit": ["megabit","BOOLEAN", {"default": true}],
|
||||
"save_metadata": ["save_metadata", "BOOLEAN", {"default": true}],
|
||||
"extension": "mp4"
|
||||
}
|
||||
@@ -2,9 +2,12 @@
|
||||
"main_pass":
|
||||
[
|
||||
"-n", "-c:v", "h264_nvenc",
|
||||
"-pix_fmt", ["pix_fmt", ["yuv420p", "yuv420p10le"]]
|
||||
"-pix_fmt", ["pix_fmt", ["yuv420p", "p010le"]],
|
||||
"-vf", "scale=out_color_matrix=bt709",
|
||||
"-color_range", "tv", "-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709"
|
||||
],
|
||||
"audio_pass": ["-c:a", "aac"],
|
||||
"fake_trc": "bt709",
|
||||
"audio_pass": ["-c:a", "aac", "-movflags", "use_metadata_tags"],
|
||||
"bitrate": ["bitrate","INT", {"default": 10, "min": 1, "max": 999, "step": 1 }],
|
||||
"megabit": ["megabit","BOOLEAN", {"default": true}],
|
||||
"save_metadata": ["save_metadata", "BOOLEAN", {"default": true}],
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
[
|
||||
"-n", "-c:v", "hevc_nvenc",
|
||||
"-vtag", "hvc1",
|
||||
"-pix_fmt", ["pix_fmt", ["yuv420p", "yuv420p10le"]]
|
||||
"-pix_fmt", ["pix_fmt", ["yuv420p", "p010le"]],
|
||||
"-vf", "scale=out_color_matrix=bt709",
|
||||
"-color_range", "tv", "-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709"
|
||||
],
|
||||
"audio_pass": ["-c:a", "aac"],
|
||||
"fake_trc": "bt709",
|
||||
"audio_pass": ["-c:a", "aac", "-movflags", "use_metadata_tags"],
|
||||
"bitrate": ["bitrate","INT", {"default": 10, "min": 1, "max": 999, "step": 1 }],
|
||||
"megabit": ["megabit","BOOLEAN", {"default": true}],
|
||||
"save_metadata": ["save_metadata", "BOOLEAN", {"default": true}],
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
"main_pass":
|
||||
[
|
||||
"-n",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-pix_fmt", ["pix_fmt",["yuv420p","yuva420p"]],
|
||||
"-crf", ["crf","INT", {"default": 20, "min": 0, "max": 100, "step": 1}],
|
||||
"-b:v", "0"
|
||||
"-b:v", "0",
|
||||
"-vf", "scale=out_color_matrix=bt709",
|
||||
"-color_range", "tv", "-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709"
|
||||
],
|
||||
"fake_trc": "bt709",
|
||||
"audio_pass": ["-c:a", "libvorbis"],
|
||||
"save_metadata": ["save_metadata", "BOOLEAN", {"default": true}],
|
||||
"trim_to_audio": ["trim_to_audio", "BOOLEAN", {"default": false}],
|
||||
"extension": "webm"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,616 @@
|
||||
from .logger import logger
|
||||
|
||||
def image(src):
|
||||
return f'<img src={src} loading=lazy style="width: 0px; min-width: 100%">'
|
||||
def video(src):
|
||||
return f'<video preload="none" src={src} muted loop controls controlslist="nodownload noremoteplayback noplaybackrate" style="width: 0px; min-width: 100%" class="VHS_loopedvideo">'
|
||||
def short_desc(desc):
|
||||
return f'<div id=VHS_shortdesc>{desc}</div>'
|
||||
|
||||
def format_each(desc, **kwargs):
|
||||
if isinstance(desc, dict):
|
||||
res = {}
|
||||
for k,v in desc.items():
|
||||
res[format_each(k, **kwargs)] = format_each(v, **kwargs)
|
||||
return res
|
||||
if isinstance(desc, list):
|
||||
res = []
|
||||
for v in desc:
|
||||
res.append(format_each(v, **kwargs))
|
||||
return res
|
||||
return desc.format(**kwargs)
|
||||
def format_type(desc, lower, lowers=None, upper=None, uppers=None, cap=None):
|
||||
"""Utility function for nodes with image/latent/mask variants"""
|
||||
if lowers is None:
|
||||
lowers = lower + 's'
|
||||
if cap is None:
|
||||
cap = lower.capitalize()
|
||||
if upper is None:
|
||||
upper = lower.upper()
|
||||
if uppers is None:
|
||||
uppers = lowers.upper()
|
||||
return format_each(desc, lower=lower, lowers=lowers, upper=upper, uppers=uppers, cap=cap)
|
||||
|
||||
common_descriptions = {
|
||||
'merge_strategy': [
|
||||
'Determines what the output resolution will be if input resolutions don\'t match',
|
||||
{'match A': 'Always use the resolution for A',
|
||||
'match B': 'Always use the resolution for B',
|
||||
'match smaller': 'Pick the smaller resolution by area',
|
||||
'match larger': 'Pick the larger resolution by area',
|
||||
}],
|
||||
'scale_method': [
|
||||
'Determines what method to use if scaling is required',
|
||||
],
|
||||
'crop_method': 'When sizes don\'t match, should the resized image have it\'s aspect ratio changed, or be cropped to maintain aspect ratio',
|
||||
'VHS_PATH': [
|
||||
'This is a VHS_PATH input. When edited, it provides a list of possible valid files or directories',
|
||||
video('https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite/assets/4284322/729b7185-1fca-41d8-bc8d-a770bb2a5ce6'),
|
||||
'The current top-most completion may be selected with Tab',
|
||||
'You can navigate up a directory by pressing Ctrl+B (or Ctrl+W if supported by browser)',
|
||||
'The filter on suggested file types can be disabled by pressing Ctrl+G.',
|
||||
'If converted to an input, this functions as a string',
|
||||
],
|
||||
"GetCount": ['Get {cap} Count 🎥🅥🅗🅢', short_desc('Return the number of {lowers} in an input as an INT'),
|
||||
{'Inputs': {
|
||||
'{lowers}': 'The input {lower}',
|
||||
},
|
||||
'Outputs': {
|
||||
'count': 'The number of {lowers} in the input',
|
||||
},
|
||||
}],
|
||||
"SelectEveryNth": ['Select Every Nth {cap} 🎥🅥🅗🅢', short_desc('Keep only 1 {lower} for every interval'),
|
||||
{'Inputs': {
|
||||
'{lowers}': 'The input {lower}',
|
||||
},
|
||||
'Outputs': {
|
||||
'{upper}': 'The output {lowers}',
|
||||
'count': 'The number of {lowers} in the input',
|
||||
},
|
||||
'Widgets':{
|
||||
'select_every_nth': 'The interval from which one frame is kept. 1 means no frames are skipped.',
|
||||
'skip_first_{lowers}': 'A number of frames which that is skipped from the start. This applies before select_every_nth. As a result, multiple copies of the node can each have a different skip_first_frames to divide the {lower} into groups'
|
||||
},
|
||||
}],
|
||||
}
|
||||
|
||||
descriptions = {
|
||||
'VHS_VideoCombine': ['Video Combine 🎥🅥🅗🅢', short_desc('Combine an image sequence into a video'), {
|
||||
'Inputs': {
|
||||
'images': 'The images to be turned into a video',
|
||||
'audio':'(optional) audio to add to the video',
|
||||
'meta_batch': '(optional) Connect to a Meta Batch manager to divide extremely long image sequences into sub batches. See the documentation for Meta Batch Manager',
|
||||
'vae':['(optional) If provided, the node will take latents as input instead of images. This drastically reduces the required RAM (not VRAM) when working with long (100+ frames) sequences',
|
||||
"Unlike on Load Video, this isn't always a strict upgrade over using a standalone VAE Decode.",
|
||||
"If you have multiple Video Combine outputs, then the VAE decode will be performed for each output node increasing execution time",
|
||||
"If you make any change to output settings on the Video Combine (such as changing the output format), the VAE decode will be performed again as the decoded result is (by design) not cached",
|
||||
]
|
||||
},
|
||||
'Widgets':{
|
||||
'frame_rate': 'The frame rate which will be used for the output video. Consider converting this to an input and connecting this to a Load Video with Video Info(Loaded)->fps. When including audio, failure to properly set this will result in audio desync',
|
||||
'loop_count': 'The number of additional times the video should repeat. Can cause performance issues when used with long (100+ frames) sequences',
|
||||
'filename_prefix': 'A prefix to add to the name of the output filename. This can include subfolders or format strings.',
|
||||
'format': 'The output format to use. Formats starting with, \'image\' are saved with PIL, but formats starting with \'video\' utilize the video_formats system. \'video\' options require ffmpeg and selecting one frequently adds additional options to the node.',
|
||||
'pingpong': 'Play the video normally, then repeat the video in reverse so that it \'pingpongs\' back and forth. This is frequently used to minimize the appearance of skips on very short animations.',
|
||||
'save_output': 'Specifies if output files should be saved to the output folder, or the temporary output folder',
|
||||
'videopreview': 'Displays a preview for the processed result. If advanced previews is enabled, the output is always converted to a format viewable from the browser. If the video has audio, it will also be previewed when moused over. Additional preview options can be accessed with right click.',
|
||||
},
|
||||
'Common Format Widgets': {
|
||||
'crf': 'Determines how much to prioritize quality over filesize. Numbers vary between formats, but on each format that includes it, the default value provides visually loss less output',
|
||||
'pix_fmt': ['The pixel format to use for output. Alternative options will often have higher quality at the cost of increased file size and reduced compatibility with external software.', {
|
||||
'yuv420p': 'The most common and default format',
|
||||
'yuv420p10le': 'Use 10 bit color depth. This can improve color quality when combined with 16bit input color depth',
|
||||
'yuva420p': 'Include transparency in the output video'
|
||||
}],
|
||||
'input_color_depth': 'VHS supports outputting 16bit images. While this produces higher quality output, the difference usually isn\'t visible without postprocessing and it significantly increases file size and processing time.',
|
||||
'save_metadata': 'Determines if metadata for the workflow should be included in the output video file',
|
||||
}
|
||||
}],
|
||||
'VHS_LoadVideo': ['Load Video 🎥🅥🅗🅢', short_desc('Loads a video from the input folder'),
|
||||
{'Inputs': {
|
||||
'meta_batch': '(optional) Connect to a Meta Batch manager to divide extremely long sequences into sub batches. See the documentation for Meta Batch Manager',
|
||||
'vae': ['(optional) If provided the node will output latents instead of images. This drastically reduces the required RAM (not VRAM) when working with long (100+ frames) sequences',
|
||||
'Using this is strongly encouraged unless connecting to a node that requires a blue image connection such as Apply Controllnet',
|
||||
],
|
||||
},
|
||||
'Outputs': {
|
||||
'IMAGE': 'The loaded images',
|
||||
'frame_count': 'The length of images just returned',
|
||||
'audio': 'The audio from the loaded video',
|
||||
'video_info': 'Exposes additional info about the video such as the source frame rate, or the total length',
|
||||
'LATENT': 'The loaded images pre-converted to latents. Only available when a vae is connected',
|
||||
},
|
||||
'Widgets': {
|
||||
'video': 'The video file to be loaded. Lists all files with a video extension in the ComfyUI/Input folder',
|
||||
'force_rate': 'Drops or duplicates frames so that the produced output has the target frame rate. Many motion models are trained on videos of a specific frame rate and will give better results if input matches that frame rate. If set to 0, all frames are returned. May give unusual results with inputs that have a variable frame rate like animated gifs. Reducing this value can also greatly reduce the execution time and memory requirements.',
|
||||
'force_size': 'Previously was used to provide suggested resolutions. Instead, custom_width and custom_height can be disabled by setting to 0.',
|
||||
'custom_width': 'Allows for an arbitrary width to be entered, cropping to maintain aspect ratio if both are set',
|
||||
'custom_height': 'Allows for an arbitrary height to be entered, cropping to maintain aspect ratio if both are set',
|
||||
'frame_load_cap': 'The maximum number of frames to load. If 0, all frames are loaded.',
|
||||
'skip_first_frames': 'A number of frames which are discarded before producing output.',
|
||||
'select_every_nth': 'Similar to frame rate. Keeps only the first of every n frames and discard the rest. Has better compatibility with variable frame rate inputs such as gifs. When combined with force_rate, select_every_nth_applies after force_rate so the resulting output has a frame rate equivalent to force_rate/select_every_nth. select_every_nth does not apply to skip_first_frames',
|
||||
'format': 'Updates other widgets so that only values supported by the given format can be entered and provides recommended defaults.',
|
||||
'choose video to upload': 'An upload button is provided to upload local files to the input folder',
|
||||
'videopreview': 'Displays a preview for the selected video input. If advanced previews is enabled, this preview will reflect the frame_load_cap, force_rate, skip_first_frames, and select_every_nth values chosen. If the video has audio, it will also be previewed when moused over. Additional preview options can be accessed with right click.',
|
||||
}
|
||||
}],
|
||||
'VHS_LoadVideoFFmpeg': ['Load Video FFmpeg 🎥🅥🅗🅢', short_desc('Loads a video from the input folder using ffmpeg instead of opencv'),
|
||||
'Provides faster execution speed, transparency support, and allows specifying start time in seconds',
|
||||
{'Inputs': {
|
||||
'meta_batch': '(optional) Connect to a Meta Batch manager to divide extremely long sequences into sub batches. See the documentation for Meta Batch Manager',
|
||||
'vae': ['(optional) If provided the node will output latents instead of images. This drastically reduces the required RAM (not VRAM) when working with long (100+ frames) sequences',
|
||||
'Using this is strongly encouraged unless connecting to a node that requires a blue image connection such as Apply Controllnet',
|
||||
],
|
||||
},
|
||||
'Outputs': {
|
||||
'IMAGE': 'The loaded images',
|
||||
'mask': 'Transparency data from the loaded video',
|
||||
'audio': 'The audio from the loaded video',
|
||||
'video_info': 'Exposes additional info about the video such as the source frame rate, or the total length',
|
||||
'LATENT': 'The loaded images pre-converted to latents. Only available when a vae is connected',
|
||||
},
|
||||
'Widgets': {
|
||||
'video': 'The video file to be loaded. Lists all files with a video extension in the ComfyUI/Input folder',
|
||||
'force_rate': 'Drops or duplicates frames so that the produced output has the target frame rate. Many motion models are trained on videos of a specific frame rate and will give better results if input matches that frame rate. If set to 0, all frames are returned. May give unusual results with inputs that have a variable frame rate like animated gifs. Reducing this value can also greatly reduce the execution time and memory requirements.',
|
||||
'force_size': 'Previously was used to provide suggested resolutions. Instead, custom_width and custom_height can be disabled by setting to 0.',
|
||||
'custom_width': 'Allows for an arbitrary width to be entered, cropping to maintain aspect ratio if both are set',
|
||||
'custom_height': 'Allows for an arbitrary height to be entered, cropping to maintain aspect ratio if both are set',
|
||||
'frame_load_cap': 'The maximum number of frames to load. If 0, all frames are loaded.',
|
||||
'start_time': 'A timestamp, in seconds from the start of the video, to start loading frames from. ',
|
||||
'format': 'Updates other widgets so that only values supported by the given format can be entered and provides recommended defaults.',
|
||||
'choose video to upload': 'An upload button is provided to upload local files to the input folder',
|
||||
'videopreview': 'Displays a preview for the selected video input. If advanced previews is enabled, this preview will reflect the frame_load_cap, force_rate, skip_first_frames, and select_every_nth values chosen. If the video has audio, it will also be previewed when moused over. Additional preview options can be accessed with right click.',
|
||||
}
|
||||
}],
|
||||
'VHS_LoadVideoPath': ['Load Video (Path) 🎥🅥🅗🅢', short_desc('Loads a video from an arbitrary path'),
|
||||
{'Inputs': {
|
||||
'meta_batch': '(optional) Connect to a Meta Batch manager to divide extremely long sequences into sub batches. See the documentation for Meta Batch Manager',
|
||||
'vae': ['(optional) If provided the node will output latents instead of images. This drastically reduces the required RAM (not VRAM) when working with long (100+ frames) sequences',
|
||||
'Using this is strongly encouraged unless connecting to a node that requires a blue image connection such as Apply Controllnet',
|
||||
],
|
||||
},
|
||||
'Outputs': {
|
||||
'IMAGE': 'The loaded images',
|
||||
'frame_count': 'The length of images just returned',
|
||||
'audio': 'The audio from the loaded video',
|
||||
'video_info': 'Exposes additional info about the video such as the source frame rate, or the total length',
|
||||
'LATENT': 'The loaded images pre-converted to latents. Only available when a vae is connected',
|
||||
},
|
||||
'Widgets': {
|
||||
'video': ['The video file to be loaded.', 'You can also select an image to load it as a single frame'] + common_descriptions['VHS_PATH'],
|
||||
'force_rate': 'Drops or duplicates frames so that the produced output has the target frame rate. Many motion models are trained on videos of a specific frame rate and will give better results if input matches that frame rate. If set to 0, all frames are returned. May give unusual results with inputs that have a variable frame rate like animated gifs. Reducing this value can also greatly reduce the execution time and memory requirements.',
|
||||
'force_size': 'Previously was used to provide suggested resolutions. Instead, custom_width and custom_height can be disabled by setting to 0.',
|
||||
'custom_width': 'Allows for an arbitrary width to be entered, cropping to maintain aspect ratio if both are set',
|
||||
'custom_height': 'Allows for an arbitrary height to be entered, cropping to maintain aspect ratio if both are set',
|
||||
'frame_load_cap': 'The maximum number of frames to load. If 0, all frames are loaded.',
|
||||
'skip_first_frames': 'A number of frames which are discarded before producing output.',
|
||||
'select_every_nth': 'Similar to frame rate. Keeps only the first of every n frames and discard the rest. Has better compatibility with variable frame rate inputs such as gifs. When combined with force_rate, select_every_nth_applies after force_rate so the resulting output has a frame rate equivalent to force_rate/select_every_nth. select_every_nth does not apply to skip_first_frames',
|
||||
'format': 'Updates other widgets so that only values supported by the given format can be entered and provides recommended defaults.',
|
||||
'videopreview': 'Displays a preview for the selected video input. Will only be shown if Advanced Previews is enabled. This preview will reflect the frame_load_cap, force_rate, skip_first_frames, and select_every_nth values chosen. If the video has audio, it will also be previewed when moused over. Additional preview options can be accessed with right click.',
|
||||
}
|
||||
}],
|
||||
'VHS_LoadVideoFFmpegPath': ['Load Video FFmpeg (Path) 🎥🅥🅗🅢', short_desc('Loads a video from an arbitrary path using ffmpeg instead of opencv'),
|
||||
'Provides faster execution speed, transparency support, and allows specifying start time in seconds',
|
||||
{'Inputs': {
|
||||
'meta_batch': '(optional) Connect to a Meta Batch manager to divide extremely long sequences into sub batches. See the documentation for Meta Batch Manager',
|
||||
'vae': ['(optional) If provided the node will output latents instead of images. This drastically reduces the required RAM (not VRAM) when working with long (100+ frames) sequences',
|
||||
'Using this is strongly encouraged unless connecting to a node that requires a blue image connection such as Apply Controllnet',
|
||||
],
|
||||
},
|
||||
'Outputs': {
|
||||
'IMAGE': 'The loaded images',
|
||||
'mask': 'Transparency data from the loaded video',
|
||||
'audio': 'The audio from the loaded video',
|
||||
'video_info': 'Exposes additional info about the video such as the source frame rate, or the total length',
|
||||
'LATENT': 'The loaded images pre-converted to latents. Only available when a vae is connected',
|
||||
},
|
||||
'Widgets': {
|
||||
'video': ['The video file to be loaded.', 'You can also select an image to load it as a single frame'] + common_descriptions['VHS_PATH'],
|
||||
'force_rate': 'Drops or duplicates frames so that the produced output has the target frame rate. Many motion models are trained on videos of a specific frame rate and will give better results if input matches that frame rate. If set to 0, all frames are returned. May give unusual results with inputs that have a variable frame rate like animated gifs. Reducing this value can also greatly reduce the execution time and memory requirements.',
|
||||
'force_size': 'Previously was used to provide suggested resolutions. Instead, custom_width and custom_height can be disabled by setting to 0.',
|
||||
'custom_width': 'Allows for an arbitrary width to be entered, cropping to maintain aspect ratio if both are set',
|
||||
'custom_height': 'Allows for an arbitrary height to be entered, cropping to maintain aspect ratio if both are set',
|
||||
'frame_load_cap': 'The maximum number of frames to load. If 0, all frames are loaded.',
|
||||
'skip_first_frames': 'A number of frames which are discarded before producing output.',
|
||||
'select_every_nth': 'Similar to frame rate. Keeps only the first of every n frames and discard the rest. Has better compatibility with variable frame rate inputs such as gifs. When combined with force_rate, select_every_nth_applies after force_rate so the resulting output has a frame rate equivalent to force_rate/select_every_nth. select_every_nth does not apply to skip_first_frames',
|
||||
'format': 'Updates other widgets so that only values supported by the given format can be entered and provides recommended defaults.',
|
||||
'videopreview': 'Displays a preview for the selected video input. Will only be shown if Advanced Previews is enabled. This preview will reflect the frame_load_cap, force_rate, skip_first_frames, and select_every_nth values chosen. If the video has audio, it will also be previewed when moused over. Additional preview options can be accessed with right click.',
|
||||
}
|
||||
}],
|
||||
'VHS_LoadImages': ['Load Images 🎥🅥🅗🅢', short_desc('Loads a sequence of images from a subdirectory of the input folder'),
|
||||
{'Inputs': {
|
||||
'meta_batch': '(optional) Connect to a Meta Batch manager to divide extremely long sequences into sub batches. See the documentation for Meta Batch Manager',
|
||||
},
|
||||
'Outputs': {
|
||||
'IMAGE': 'The loaded images',
|
||||
'MASK': 'The alpha channel of the loaded images.',
|
||||
'frame_count': 'The length of images just returned',
|
||||
},
|
||||
'Widgets': {
|
||||
'directory': 'The directory images will be loaded from. Filtered to process jpg, png, ppm, bmp, tif, and webp files',
|
||||
'image_load_cap': 'The maximum number of images to load. If 0, all images are loaded.',
|
||||
'start_time': 'A timestamp, in seconds from the start of the video, to start loading frames from. ',
|
||||
'choose folder to upload': 'An upload button is provided to upload a local folder containing images to the input folder',
|
||||
'videopreview': 'Displays a preview for the selected video input. Will only be shown if Advanced Previews is enabled. This preview will reflect the image_load_cap, skip_first_images, and select_every_nth values chosen. Additional preview options can be accessed with right click.',
|
||||
}
|
||||
}],
|
||||
'VHS_LoadImagesPath': ['Load Images (Path) 🎥🅥🅗🅢', short_desc('Loads a sequence of images from an arbitrary path'),
|
||||
{'Inputs': {
|
||||
'meta_batch': '(optional) Connect to a Meta Batch manager to divide extremely long sequences into sub batches. See the documentation for Meta Batch Manager',
|
||||
},
|
||||
'Outputs': {
|
||||
'IMAGE': 'The loaded images',
|
||||
'MASK': 'The alpha channel of the loaded images.',
|
||||
'frame_count': 'The length of images just returned',
|
||||
},
|
||||
'Widgets': {
|
||||
'directory': ['The directory images will be loaded from. Filtered to process jpg, png, ppm, bmp, tif, and webp files'] + common_descriptions['VHS_PATH'],
|
||||
'image_load_cap': 'The maximum number of images to load. If 0, all images are loaded.',
|
||||
'skip_first_images': 'A number of images which are discarded before producing output.',
|
||||
'select_every_nth': 'Keeps only the first of every n frames and discard the rest.',
|
||||
'videopreview': 'Displays a preview for the selected video input. Will only be shown if Advanced Previews is enabled. This preview will reflect the image_load_cap, skip_first_images, and select_every_nth values chosen. Additional preview options can be accessed with right click.',
|
||||
}
|
||||
}],
|
||||
'VHS_LoadImagePath': ['Load Image (Path) 🎥🅥🅗🅢', short_desc('Load a single image from a given path'),
|
||||
{'Inputs': {
|
||||
'vae': '(optional) If provided the node will output latents instead of images.',
|
||||
},
|
||||
'Outputs': {
|
||||
'IMAGE': 'The loaded images',
|
||||
'MASK': 'The alpha channel of the loaded images.',
|
||||
},
|
||||
'Widgets': {
|
||||
'image': ['The image file to be loaded.'] + common_descriptions['VHS_PATH'],
|
||||
'force_size': ['Allows for conveniently scaling the input without requiring an additional node. Provides options to maintain aspect ratio or conveniently target common training formats for Animate Diff', {'custom_width': 'Allows for an arbitrary width to be entered, cropping to maintain aspect ratio if both are set',
|
||||
'custom_height': 'Allows for an arbitrary height to be entered, cropping to maintain aspect ratio if both are set'}],
|
||||
'videopreview': 'Displays a preview for the selected video input. Will only be shown if Advanced Previews is enabled. This preview will reflect the image_load_cap, skip_first_images, and select_every_nth values chosen. Additional preview options can be accessed with right click.',
|
||||
}
|
||||
}],
|
||||
"VHS_LoadAudio": ['Load Audio (Path) 🎥🅥🅗🅢', short_desc('Loads an audio file from an arbitrary path'),
|
||||
{'Outputs': {
|
||||
'audio': 'The loaded audio',
|
||||
},
|
||||
'Widgets': {
|
||||
'audio_file': ['The audio file to be loaded.'] + common_descriptions['VHS_PATH'],
|
||||
'seek_seconds': 'An offset from the start of the sound file that the audio should start from',
|
||||
}
|
||||
}],
|
||||
"VHS_LoadAudioUpload": ['Load Audio (Upload) 🎥🅥🅗🅢', short_desc('Loads an audio file from the input directory'),
|
||||
"Very similar in functionality to the built-in LoadAudio. It was originally added before VHS swapped to use Comfy's internal AUDIO format, but provides the additional options for start time and duration",
|
||||
{'Outputs': {
|
||||
'audio': 'The loaded audio',
|
||||
},
|
||||
'Widgets': {
|
||||
'audio': 'The audio file to be loaded.',
|
||||
'start_time': 'An offset from the start of the sound file that the audio should start from',
|
||||
'duration': 'A maximum limit for the audio. Disabled if 0',
|
||||
'choose audio to upload': 'An upload button is provided to upload an audio file to the input folder',
|
||||
}
|
||||
}],
|
||||
"VHS_AudioToVHSAudio": ['Audio to legacy VHS_AUDIO 🎥🅥🅗🅢', short_desc('utility function for compatibility with external nodes'),
|
||||
"VHS used to use an internal VHS_AUDIO format for routing audio between inputs and outputs. This format was intended to only be used internally and was designed with a focus on performance over ease of use. Since ComfyUI now has an internal AUDIO format, VHS now uses this format. However, some custom node packs were made that are external to both ComfyUI and VHS that use VHS_AUDIO. This node was added so that those external nodes can still function",
|
||||
{'Inputs': {
|
||||
'audio': 'An input in the standardized AUDIO format',
|
||||
},
|
||||
'Outputs': {
|
||||
'vhs_audio': 'An output in the legacy VHS_AUDIO format for use with external nodes',
|
||||
},
|
||||
}],
|
||||
"VHS_VHSAudioToAudio": ['Legacy VHS_AUDIO to Audio 🎥🅥🅗🅢', short_desc('utility function for compatibility with external nodes'),
|
||||
"VHS used to use an internal VHS_AUDIO format for routing audio between inputs and outputs. This format was intended to only be used internally and was designed with a focus on performance over ease of use. Since ComfyUI now has an internal AUDIO format, VHS now uses this format. However, some custom node packs were made that are external to both ComfyUI and VHS that use VHS_AUDIO. This node was added so that those external nodes can still function",
|
||||
{'Inputs': {
|
||||
'vhs_audio': 'An input in the legacy VHS_AUDIO format produced by an external node',
|
||||
},
|
||||
'Outputs': {
|
||||
'vhs_audio': 'An output in the standardized AUDIO format',
|
||||
},
|
||||
}],
|
||||
"VHS_PruneOutputs": ['Prune Outputs 🎥🅥🅗🅢', short_desc('Automates deletion of undesired outputs from a Video Combine node.'),
|
||||
'Video Combine produces a number of file outputs in addition to the final output. Some of these, such as a video file without audio included, are implementation limitations and are not feasible to solve. As an alternative, the Prune Outputs node is added to automate the deletion of these file outputs if they are not desired',
|
||||
{'Inputs': {
|
||||
'filenames': 'A connection from a Video Combine node to indicate which outputs should be pruned',
|
||||
},
|
||||
'Widgets': {
|
||||
'options': ['Which files should be deleted',
|
||||
{'Intermediate': 'Delete any files that were required for intermediate processing but are not the final output, like the no-audio output file when audio is included',
|
||||
'Intermediate and Utility': 'Delete all produced files that aren\'t the final output, including the first frame png',
|
||||
}]}
|
||||
}],
|
||||
"VHS_BatchManager": ['Meta Batch Manager 🎥🅥🅗🅢', short_desc('Split the processing of a very long video into sets of smaller Meta Batches'),
|
||||
"The Meta Batch Manager allows for extremely long input videos to be processed when all other methods for fitting the content in RAM fail. It does not effect VRAM usage.",
|
||||
"It must be connected to at least one Input (a Load Video or Load Images) AND at least one Video Combine",
|
||||
image("https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite/assets/4284322/7cb3fb7e-59d8-4cb2-a09f-9c6698de8b1f"),
|
||||
"It functions by holding both the inputs and ouputs open between executions, and automatically requeue's the workflow until one of the inputs is unable to provide additional images.",
|
||||
"Because each sub execution only contains a subset of the total frames, each sub execution creates a hard window which temporal smoothing can not be applied across. This results in jumps in the output.",
|
||||
{'Outputs': {
|
||||
'meta_batch': 'Add all connected nodes to this Meta Batch',
|
||||
},
|
||||
'Widgets': {
|
||||
'frames_per_batch': 'How many frames to process for each sub execution. If loading as image, each frame will use about 50MB of RAM (not VRAM), and this can safely be set in the 100-1000 range, depending on available memory. When loading and combining from latent space (no blue image noodles exist), this value can be much higher, around the 2,000 to 20,000 range',
|
||||
}
|
||||
}],
|
||||
"VHS_VideoInfo": ['Video Info 🎥🅥🅗🅢', short_desc('Splits information on a video into a numerous outputs'),
|
||||
{'Inputs': {
|
||||
'video_info': 'A connection to a Load Video node',
|
||||
},
|
||||
'Outputs': {
|
||||
'source_fps🟨': 'The frame rate of the video',
|
||||
'source_frame_count🟨': 'How many total frames the video contains before accounting for frame rate or select_every_nth',
|
||||
'source_duration🟨': 'The length of images just returned in seconds',
|
||||
'source_width🟨': 'The width',
|
||||
'source_height🟨': 'The height',
|
||||
'loaded_fps🟦': 'The frame rate after accounting for force_rate and select_every_nth. This output is of particular use as it can be connected to the converted frame_rate input of a Video Combine node to ensure audio remains synchronized.',
|
||||
'loaded_frame_count🟦': 'The number of frames returned by the current execution. Identical to the frame_count returned by the node itself',
|
||||
'loaded_duration🟦': 'The duration in seconds of returned images after accounting for frame_load_cap',
|
||||
'loaded_width🟦': 'The width of the video after scaling. These coordinates are in image space even if loading to latent space',
|
||||
'loaded_height🟦': 'The height of the video after scaling. These coordinates are in image space even if loading to latent space',
|
||||
},
|
||||
}],
|
||||
"VHS_VideoInfoSource": ['Video Info Source 🎥🅥🅗🅢', short_desc('Splits information on a video into a numerous outputs describing the file itself without accounting for load options'),
|
||||
{'Inputs': {
|
||||
'video_info': 'A connection to a Load Video node',
|
||||
},
|
||||
'Outputs': {
|
||||
'source_fps🟨': 'The frame rate of the video',
|
||||
'source_frame_count🟨': 'How many total frames the video contains before accounting for frame rate or select_every_nth',
|
||||
'source_duration🟨': 'The length of images just returned in seconds',
|
||||
'source_width🟨': 'The original width',
|
||||
'source_height🟨': 'The original height',
|
||||
}
|
||||
}],
|
||||
"VHS_VideoInfoLoaded": ['Video Info Loaded 🎥🅥🅗🅢', short_desc('Splits information on a video into a numerous outputs describing the file itself after accounting for load options'),
|
||||
{'Inputs': {
|
||||
'video_info': 'A connection to a Load Video node',
|
||||
},
|
||||
'Outputs': {
|
||||
'loaded_fps🟦': 'The frame rate after accounting for force_rate and select_every_nth. This output is of particular use as it can be connected to the converted frame_rate input of a Video Combine node to ensure audio remains synchronized.',
|
||||
'loaded_frame_count🟦': 'The number of frames returned by the current execution. Identical to the frame_count returned by the node itself',
|
||||
'loaded_duration🟦': 'The duration in seconds of returned images after accounting for frame_load_cap',
|
||||
'loaded_width🟦': 'The width of the video after scaling. This is the dimension of the corresponding image even if loading as a latent directly',
|
||||
'loaded_height🟦': 'The height of the video after scaling. This is the dimension of the corresponding image even if loading as a latent directly',
|
||||
}
|
||||
}],
|
||||
"VHS_SelectFilename": ['VAE Select Filename 🎥🅥🅗🅢', short_desc('Select a single filename from the VHS_FILENAMES output by a Video Combine and return it as a string'),
|
||||
'Take care when combining this node with Prune Outputs. The VHS_FILENAMES object is immutable and will always contain the full list of output files, but execution order is undefined behavior (currently, Prune Outputs will generally execute first) and SelectFilename may return a path to a file that no longer exists.',
|
||||
{'Inputs': {
|
||||
'filenames': 'A VHS_FILENAMES from a Video Combine node',
|
||||
},
|
||||
'Outputs': {
|
||||
'filename': 'A string representation of the full output path for the chosen file',
|
||||
},
|
||||
'Widgets': {
|
||||
'index': 'The index of which file should be selected. The default, -1, chooses the most complete output',
|
||||
},
|
||||
}],
|
||||
# Batched Nodes
|
||||
"VHS_VAEEncodeBatched": ['VAE Encode Batched 🎥🅥🅗🅢', short_desc('Encode images as latents with a manually specified batch size.'),
|
||||
"Some people have ran into VRAM issues when encoding or decoding large batches of images. As a workaround, this node lets you manually set a batch size when encoding images.",
|
||||
"Unless these issues have been encountered, it is simpler to use the native VAE Encode or to encode directly from a Load Video",
|
||||
{'Inputs': {
|
||||
'pixels': 'The images to be encoded.',
|
||||
'vae': 'The VAE to use when encoding.',
|
||||
},
|
||||
'Outputs': {
|
||||
'LATENT': 'The encoded latents.',
|
||||
},
|
||||
'Widgets': {
|
||||
'per_batch': 'The maximum number of images to encode in each batch.',
|
||||
},
|
||||
}],
|
||||
"VHS_VAEDecodeBatched": ['VAE Decode Batched 🎥🅥🅗🅢', short_desc('Decode latents to images with a manually specified batch size'),
|
||||
"Some people have ran into VRAM issues when encoding or decoding large batches of images. As a workaround, this node lets you manually set a batch size when decoding latents.",
|
||||
"Unless these issues have been encountered, it is simpler to use the native VAE Decode or to decode from a Video Combine directly",
|
||||
{'Inputs': {
|
||||
'samples': 'The latents to be decoded.',
|
||||
'vae': 'The VAE to use when decoding.',
|
||||
},
|
||||
'Outputs': {
|
||||
'IMAGE': 'The decoded images.',
|
||||
},
|
||||
'Widgets': {
|
||||
'per_batch': 'The maximum number of images to decode in each batch.',
|
||||
},
|
||||
}],
|
||||
# Latent and Image nodes
|
||||
"VHS_SplitLatents": ['Split Latents 🎥🅥🅗🅢', short_desc('Split a set of latents into two groups'),
|
||||
{'Inputs': {
|
||||
'latents': 'The latents to be split.',
|
||||
},
|
||||
'Outputs': {
|
||||
'LATENT_A': 'The first group of latents',
|
||||
'A_count': 'The number of latents in group A. This will be equal to split_index unless the latents input has length less than split_index',
|
||||
'LATENT_B': 'The second group of latents',
|
||||
'B_count': 'The number of latents in group B'
|
||||
},
|
||||
'Widgets': {
|
||||
'split_index': 'The index of the first latent that will be in the second output groups.',
|
||||
},
|
||||
|
||||
}],
|
||||
"VHS_SplitImages": ['Split Images 🎥🅥🅗🅢', short_desc('Split a set of images into two groups'),
|
||||
{'Inputs': {
|
||||
'images': 'The images to be split.',
|
||||
},
|
||||
'Outputs': {
|
||||
'IMAGE_A': 'The first group of images',
|
||||
'A_count': 'The number of images in group A. This will be equal to split_index unless the images input has length less than split_index',
|
||||
'IMAGE_B': 'The second group of images',
|
||||
'B_count': 'The number of images in group B'
|
||||
},
|
||||
'Widgets': {
|
||||
'split_index': 'The index of the first latent that will be in the second output groups.',
|
||||
},
|
||||
|
||||
}],
|
||||
"VHS_SplitMasks": ['Split Masks 🎥🅥🅗🅢', short_desc('Split a set of masks into two groups'),
|
||||
{'Inputs': {
|
||||
'mask': 'The masks to be split.',
|
||||
},
|
||||
'Outputs': {
|
||||
'MASK_A': 'The first group of masks',
|
||||
'A_count': 'The number of masks in group A. This will be equal to split_index unless the mask input has length less than split_index',
|
||||
'MASK_B': 'The second group of masks',
|
||||
'B_count': 'The number of masks in group B'
|
||||
},
|
||||
'Widgets': {
|
||||
'split_index': 'The index of the first latent that will be in the second output groups.',
|
||||
},
|
||||
|
||||
}],
|
||||
"VHS_MergeLatents": ['Merge Latents 🎥🅥🅗🅢', short_desc('Combine two groups of latents into a single group of latents'),
|
||||
{'Inputs': {
|
||||
'latents_A': 'The first group of latents',
|
||||
'latents_B': 'The first group of latents',
|
||||
},
|
||||
'Outputs': {
|
||||
'LATENT': 'The combined group of latents',
|
||||
'count': 'The length of the combined group',
|
||||
},
|
||||
'Widgets': {
|
||||
'merge_strategy': common_descriptions['merge_strategy'],
|
||||
'scale_method': common_descriptions['scale_method'],
|
||||
'crop': common_descriptions['crop_method'],
|
||||
},
|
||||
|
||||
}],
|
||||
"VHS_MergeImages": ['Merge Images 🎥🅥🅗🅢', short_desc('Combine two groups of images into a single group of images'),
|
||||
{'Inputs': {
|
||||
'images_A': 'The first group of images',
|
||||
'images_B': 'The first group of images',
|
||||
},
|
||||
'Outputs': {
|
||||
'IMAGE': 'The combined group of images',
|
||||
'count': 'The length of the combined group',
|
||||
},
|
||||
'Widgets': {
|
||||
'merge_strategy': common_descriptions['merge_strategy'],
|
||||
'scale_method': common_descriptions['scale_method'],
|
||||
'crop': common_descriptions['crop_method'],
|
||||
},
|
||||
|
||||
}],
|
||||
"VHS_MergeMasks": ['Merge Masks 🎥🅥🅗🅢', short_desc('Combine two groups of masks into a single group of masks'),
|
||||
{'Inputs': {
|
||||
'mask_A': 'The first group of masks',
|
||||
'mask_B': 'The first group of masks',
|
||||
},
|
||||
'Outputs': {
|
||||
'MASK': 'The combined group of masks',
|
||||
'count': 'The length of the combined group',
|
||||
},
|
||||
'Widgets': {
|
||||
'merge_strategy': common_descriptions['merge_strategy'],
|
||||
'scale_method': common_descriptions['scale_method'],
|
||||
'crop': common_descriptions['crop_method'],
|
||||
},
|
||||
|
||||
}],
|
||||
"VHS_GetLatentCount": format_type(common_descriptions['GetCount'], 'latent'),
|
||||
"VHS_GetImageCount": format_type(common_descriptions['GetCount'], 'image'),
|
||||
"VHS_GetMaskCount": format_type(common_descriptions['GetCount'], 'mask'),
|
||||
"VHS_DuplicateLatents": ['Repeat Latents 🎥🅥🅗🅢', short_desc('Append copies of a latent to itself so it repeats'),
|
||||
{'Inputs': {
|
||||
'latents': 'The latents to be repeated',
|
||||
},
|
||||
'Outputs': {
|
||||
'LATENT': 'The latent with repeats',
|
||||
'count': 'The number of latents in the output. Equal to the length of the input latent * multiply_by',
|
||||
},
|
||||
'Widgets': {
|
||||
'multiply_by': 'Controls the number of times the latent should repeat. 1, the default, means no change.',
|
||||
},
|
||||
}],
|
||||
"VHS_DuplicateImages": ['Repeat Images 🎥🅥🅗🅢', short_desc('Append copies of a image to itself so it repeats'),
|
||||
{'Inputs': {
|
||||
'IMAGES': 'The image to be repeated',
|
||||
},
|
||||
'Outputs': {
|
||||
'IMAGE': 'The image with repeats',
|
||||
'count': 'The number of image in the output. Equal to the length of the input image * multiply_by',
|
||||
},
|
||||
'Widgets': {
|
||||
'multiply_by': 'Controls the number of times the mask should repeat. 1, the default, means no change.',
|
||||
},
|
||||
}],
|
||||
"VHS_DuplicateMasks": ['Repeat Masks 🎥🅥🅗🅢', short_desc('Append copies of a mask to itself so it repeats'),
|
||||
{'Inputs': {
|
||||
'masks': 'The masks to be repeated',
|
||||
},
|
||||
'Outputs': {
|
||||
'LATENT': 'The mask with repeats',
|
||||
'count': 'The number of mask in the output. Equal to the length of the input mask * multiply_by',
|
||||
},
|
||||
'Widgets': {
|
||||
'multiply_by': 'Controls the number of times the mask should repeat. 1, the default, means no change.',
|
||||
},
|
||||
}],
|
||||
"VHS_SelectEveryNthLatent": format_type(common_descriptions['SelectEveryNth'], 'latent'),
|
||||
"VHS_SelectEveryNthImage": format_type(common_descriptions['SelectEveryNth'], 'image'),
|
||||
#TODO: fix discrepency of input being mask instead of masks?
|
||||
"VHS_SelectEveryNthMask": format_type(common_descriptions['SelectEveryNth'], 'mask', lowers='mask'),
|
||||
#TODO: port documentation for select nodes to new system
|
||||
#"VHS_SelectLatents": None,
|
||||
#"VHS_SelectImages": None,
|
||||
#"VHS_SelectMasks": None,
|
||||
"VHS_Unbatch": ['Unbatch 🎥🅥🅗🅢', short_desc('Unbatch a list of items into a single concatenated item'),
|
||||
"Useful for when you want a single video output from a complex workflow",
|
||||
"Has no relation to the Meta Batch system of VHS",
|
||||
{'Inputs': {
|
||||
'batched': 'Any input which may or may not be batched',
|
||||
},
|
||||
'Outputs': {
|
||||
'unbatched': 'A single output element. Torch tensors are concatenated across dim 0, all other types are added which functions as concatenation for strings and arrays, but may give undesired results for other types',
|
||||
},
|
||||
}],
|
||||
"VHS_SelectLatest": ['Select Latest 🎥🅥🅗🅢', short_desc('Experimental virtual node to select the most recently modified file from a given folder'),
|
||||
"Assists in the creation of workflows where outputs from one execution are used elsewhere in subsequent executions.",
|
||||
{'Inputs': {
|
||||
'filename_prefix': 'A path which can consist of a combination of folders and a prefix which candidate files must match',
|
||||
'filename_postfix': 'A string which chich the selected file must end with. Useful for limiting to a target extension.',
|
||||
},
|
||||
'Outputs': {
|
||||
'Filename': 'A string representing a file path to the most recently modified file.',
|
||||
},
|
||||
}],
|
||||
}
|
||||
|
||||
def as_html(entry, depth=0):
|
||||
if isinstance(entry, dict):
|
||||
size = 0.8 if depth < 2 else 1
|
||||
html = ''
|
||||
for k in entry:
|
||||
if k == "collapsed":
|
||||
continue
|
||||
collapse_single = k.endswith("_collapsed")
|
||||
if collapse_single:
|
||||
name = k[:-len("_collapsed")]
|
||||
else:
|
||||
name = k
|
||||
collapse_flag = ' VHS_precollapse' if entry.get("collapsed", False) or collapse_single else ''
|
||||
html += f'<div vhs_title=\"{name}\" style=\"display: flex; font-size: {size}em\" class=\"VHS_collapse{collapse_flag}\"><div style=\"color: #AAA; height: 1.5em;\">[<span style=\"font-family: monospace\">-</span>]</div><div style=\"width: 100%\">{name}: {as_html(entry[k], depth=depth+1)}</div></div>'
|
||||
return html
|
||||
if isinstance(entry, list):
|
||||
if depth == 0:
|
||||
depth += 1
|
||||
size = .8
|
||||
else:
|
||||
size = 1
|
||||
html = ''
|
||||
html += entry[0]
|
||||
for i in entry[1:]:
|
||||
html += f'<div style=\"font-size: {size}em\">{as_html(i, depth=depth)}</div>'
|
||||
return html
|
||||
return str(entry)
|
||||
|
||||
def format_descriptions(nodes):
|
||||
for k in descriptions:
|
||||
if k.endswith("_collapsed"):
|
||||
k = k[:-len("_collapsed")]
|
||||
nodes[k].DESCRIPTION = as_html(descriptions[k])
|
||||
undocumented_nodes = []
|
||||
for k in nodes:
|
||||
if not hasattr(nodes[k], "DESCRIPTION"):
|
||||
undocumented_nodes.append(k)
|
||||
if len(undocumented_nodes) > 0:
|
||||
logger.warning('Some nodes have not been documented %s', undocumented_nodes)
|
||||
|
||||
@@ -3,7 +3,7 @@ import torch
|
||||
|
||||
import comfy.utils
|
||||
|
||||
from .utils import BIGMIN, BIGMAX
|
||||
from .utils import BIGMIN, BIGMAX, select_indexes_from_str, convert_str_to_indexes, select_indexes
|
||||
|
||||
|
||||
class MergeStrategies:
|
||||
@@ -48,13 +48,15 @@ class SplitLatents:
|
||||
RETURN_NAMES = ("LATENT_A", "A_count", "LATENT_B", "B_count")
|
||||
FUNCTION = "split_latents"
|
||||
|
||||
def split_latents(self, latents: dict, split_index: int):
|
||||
latents = latents.copy()
|
||||
group_a = latents["samples"][:split_index]
|
||||
group_b = latents["samples"][split_index:]
|
||||
group_a_latent = {"samples": group_a}
|
||||
group_b_latent = {"samples": group_b}
|
||||
return (group_a_latent, group_a.size(0), group_b_latent, group_b.size(0))
|
||||
def split_latents(self, latents: dict[str, Tensor], split_index: int):
|
||||
latents_len = len(latents["samples"])
|
||||
group_a = latents.copy()
|
||||
group_b = latents.copy()
|
||||
for key, val in latents.items():
|
||||
if type(val) == Tensor and len(val) == latents_len:
|
||||
group_a[key] = latents[key][:split_index]
|
||||
group_b[key] = latents[key][split_index:]
|
||||
return (group_a, group_a["samples"].size(0), group_b, group_b["samples"].size(0))
|
||||
|
||||
|
||||
class SplitImages:
|
||||
@@ -125,6 +127,7 @@ class MergeLatents:
|
||||
latents_A = latents_A.copy()["samples"]
|
||||
latents_B = latents_B.copy()["samples"]
|
||||
|
||||
# TODO: handle other properties on latents besides just "samples"
|
||||
# if not same dimensions, do scaling
|
||||
if latents_A.shape[3] != latents_B.shape[3] or latents_A.shape[2] != latents_B.shape[2]:
|
||||
A_size = latents_A.shape[3] * latents_A.shape[2]
|
||||
@@ -271,9 +274,13 @@ class SelectEveryNthLatent:
|
||||
RETURN_NAMES = ("LATENT", "count",)
|
||||
FUNCTION = "select_latents"
|
||||
|
||||
def select_latents(self, latents: dict, select_every_nth: int, skip_first_latents: int):
|
||||
sub_latents = latents.copy()["samples"][skip_first_latents::select_every_nth]
|
||||
return ({"samples": sub_latents}, sub_latents.size(0))
|
||||
def select_latents(self, latents: dict[str, Tensor], select_every_nth: int, skip_first_latents: int):
|
||||
latents = latents.copy()
|
||||
latents_len = len(latents["samples"])
|
||||
for key, val in latents.items():
|
||||
if type(val) == Tensor and len(val) == latents_len:
|
||||
latents[key] = val[skip_first_latents::select_every_nth]
|
||||
return (latents, latents["samples"].size(0))
|
||||
|
||||
|
||||
class SelectEveryNthImage:
|
||||
@@ -378,7 +385,7 @@ class GetMaskCount:
|
||||
return (mask.size(0),)
|
||||
|
||||
|
||||
class DuplicateLatents:
|
||||
class RepeatLatents:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
@@ -395,15 +402,18 @@ class DuplicateLatents:
|
||||
FUNCTION = "duplicate_input"
|
||||
|
||||
def duplicate_input(self, latents: dict[str, Tensor], multiply_by: int):
|
||||
new_latents = latents.copy()
|
||||
full_latents = []
|
||||
for n in range(0, multiply_by):
|
||||
full_latents.append(new_latents["samples"])
|
||||
new_latents["samples"] = torch.cat(full_latents, dim=0)
|
||||
return (new_latents, new_latents["samples"].size(0),)
|
||||
latents = latents.copy()
|
||||
latents_len = len(latents["samples"])
|
||||
for key, val in latents.items():
|
||||
if type(val) == Tensor and len(val) == latents_len:
|
||||
full_latents = []
|
||||
for _ in range(0, multiply_by):
|
||||
full_latents.append(latents[key])
|
||||
latents[key] = torch.cat(full_latents, dim=0)
|
||||
return (latents, latents["samples"].size(0),)
|
||||
|
||||
|
||||
class DuplicateImages:
|
||||
class RepeatImages:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
@@ -427,7 +437,7 @@ class DuplicateImages:
|
||||
return (new_images, new_images.size(0),)
|
||||
|
||||
|
||||
class DuplicateMasks:
|
||||
class RepeatMasks:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
@@ -451,12 +461,94 @@ class DuplicateMasks:
|
||||
return (new_mask, new_mask.size(0),)
|
||||
|
||||
|
||||
# class SelectLatents:
|
||||
# @classmethod
|
||||
# def INPUT_TYPES(s):
|
||||
# return {
|
||||
# "required": {
|
||||
# "images": ("IMAGE",),
|
||||
# "select_indeces": ("STRING", {"default": ""}),
|
||||
# },
|
||||
# }
|
||||
select_description = """Use comma-separated indexes to select items in the given order.
|
||||
Supports negative indexes, python-style ranges (end index excluded),
|
||||
as well as range step.
|
||||
|
||||
Acceptable entries (assuming 16 items provided, so idxs 0 to 15 exist):
|
||||
0 -> Returns [0]
|
||||
-1 -> Returns [15]
|
||||
0, 1, 13 -> Returns [0, 1, 13]
|
||||
0:5, 13 -> Returns [0, 1, 2, 3, 4, 13]
|
||||
0:-1 -> Returns [0, 1, 2, ..., 13, 14]
|
||||
0:5:-1 -> Returns [4, 3, 2, 1, 0]
|
||||
0:5:2 -> Returns [0, 2, 4]
|
||||
::-1 -> Returns [15, 14, 13, ..., 2, 1, 0]
|
||||
"""
|
||||
class SelectLatents:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"latent": ("LATENT",),
|
||||
"indexes": ("STRING", {"default": "0"}),
|
||||
"err_if_missing": ("BOOLEAN", {"default": True}),
|
||||
"err_if_empty": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
}
|
||||
|
||||
DESCRIPTION = select_description
|
||||
CATEGORY = "Video Helper Suite 🎥🅥🅗🅢/latent"
|
||||
|
||||
RETURN_TYPES = ("LATENT",)
|
||||
FUNCTION = "select"
|
||||
|
||||
def select(self, latent: dict[str, Tensor], indexes: str, err_if_missing: bool, err_if_empty: bool):
|
||||
# latents are a dict and may contain different stuff (like noise_mask), so need to account for it all
|
||||
latent = latent.copy()
|
||||
latents_len = len(latent["samples"])
|
||||
real_idxs = convert_str_to_indexes(indexes, latents_len, allow_missing=not err_if_missing)
|
||||
if err_if_empty and len(real_idxs) == 0:
|
||||
raise Exception(f"Nothing was selected based on indexes found in '{indexes}'.")
|
||||
for key, val in latent.items():
|
||||
if type(val) == Tensor and len(val) == latents_len:
|
||||
latent[key] = select_indexes(val, real_idxs)
|
||||
return (latent,)
|
||||
|
||||
|
||||
class SelectImages:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"indexes": ("STRING", {"default": "0"}),
|
||||
"err_if_missing": ("BOOLEAN", {"default": True}),
|
||||
"err_if_empty": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
}
|
||||
|
||||
DESCRIPTION = select_description
|
||||
CATEGORY = "Video Helper Suite 🎥🅥🅗🅢/image"
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "select"
|
||||
|
||||
def select(self, image: Tensor, indexes: str, err_if_missing: bool, err_if_empty: bool):
|
||||
to_return = select_indexes_from_str(input_obj=image, indexes=indexes,
|
||||
err_if_missing=err_if_missing, err_if_empty=err_if_empty)
|
||||
to_return_type = type(to_return)
|
||||
return (to_return,)
|
||||
|
||||
|
||||
class SelectMasks:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"mask": ("MASK",),
|
||||
"indexes": ("STRING", {"default": "0"}),
|
||||
"err_if_missing": ("BOOLEAN", {"default": True}),
|
||||
"err_if_empty": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
}
|
||||
|
||||
DESCRIPTION = select_description
|
||||
CATEGORY = "Video Helper Suite 🎥🅥🅗🅢/mask"
|
||||
|
||||
RETURN_TYPES = ("MASK",)
|
||||
FUNCTION = "select"
|
||||
|
||||
def select(self, mask: Tensor, indexes: str, err_if_missing: bool, err_if_empty: bool):
|
||||
return (select_indexes_from_str(input_obj=mask, indexes=indexes,
|
||||
err_if_missing=err_if_missing, err_if_empty=err_if_empty),)
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
from PIL import Image
|
||||
import time
|
||||
import io
|
||||
import struct
|
||||
from threading import Thread
|
||||
import torch.nn.functional as F
|
||||
import torch
|
||||
|
||||
import latent_preview
|
||||
import server
|
||||
serv = server.PromptServer.instance
|
||||
|
||||
from .utils import hook
|
||||
|
||||
rates_table = {'Mochi': 24//6, 'LTXV': 24//8, 'HunyuanVideo': 24//4,
|
||||
'Cosmos1CV8x8x8': 24//8, 'Wan21': 16//4, 'Wan22': 24//4}
|
||||
|
||||
class WrappedPreviewer(latent_preview.LatentPreviewer):
|
||||
def __init__(self, previewer, rate=8):
|
||||
self.first_preview = True
|
||||
self.last_time = 0
|
||||
self.c_index = 0
|
||||
self.rate = rate
|
||||
if hasattr(previewer, 'taesd'):
|
||||
self.taesd = previewer.taesd
|
||||
elif hasattr(previewer, 'latent_rgb_factors'):
|
||||
self.latent_rgb_factors = previewer.latent_rgb_factors
|
||||
self.latent_rgb_factors_bias = previewer.latent_rgb_factors_bias
|
||||
self.latent_rgb_factors_reshape = getattr(previewer, 'latent_rgb_factors_reshape', None)
|
||||
else:
|
||||
raise Exception('Unsupported preview type for VHS animated previews')
|
||||
|
||||
def decode_latent_to_preview_image(self, preview_format, x0):
|
||||
if x0.ndim == 5:
|
||||
#Keep batch major
|
||||
x0 = x0.movedim(2,1)
|
||||
x0 = x0.reshape((-1,)+x0.shape[-3:])
|
||||
num_images = x0.size(0)
|
||||
new_time = time.time()
|
||||
num_previews = int((new_time - self.last_time) * self.rate)
|
||||
self.last_time = self.last_time + num_previews/self.rate
|
||||
if num_previews > num_images:
|
||||
num_previews = num_images
|
||||
elif num_previews <= 0:
|
||||
return None
|
||||
if self.first_preview:
|
||||
self.first_preview = False
|
||||
serv.send_sync('VHS_latentpreview', {'length':num_images, 'rate': self.rate, 'id': serv.last_node_id})
|
||||
self.last_time = new_time + 1/self.rate
|
||||
if self.c_index + num_previews > num_images:
|
||||
x0 = x0.roll(-self.c_index, 0)[:num_previews]
|
||||
else:
|
||||
x0 = x0[self.c_index:self.c_index + num_previews]
|
||||
Thread(target=self.process_previews, args=(x0, self.c_index,
|
||||
num_images)).run()
|
||||
self.c_index = (self.c_index + num_previews) % num_images
|
||||
return None
|
||||
def process_previews(self, image_tensor, ind, leng):
|
||||
image_tensor = self.decode_latent_to_preview(image_tensor)
|
||||
if image_tensor.size(1) > 512 or image_tensor.size(2) > 512:
|
||||
image_tensor = image_tensor.movedim(-1,0)
|
||||
if image_tensor.size(2) < image_tensor.size(3):
|
||||
height = (512 * image_tensor.size(2)) // image_tensor.size(3)
|
||||
image_tensor = F.interpolate(image_tensor, (height,512), mode='bilinear')
|
||||
else:
|
||||
width = (512 * image_tensor.size(3)) // image_tensor.size(2)
|
||||
image_tensor = F.interpolate(image_tensor, (512, width), mode='bilinear')
|
||||
image_tensor = image_tensor.movedim(0,-1)
|
||||
previews_ubyte = (((image_tensor + 1.0) / 2.0).clamp(0, 1) # change scale from -1..1 to 0..1
|
||||
.mul(0xFF) # to 0..255
|
||||
).to(device="cpu", dtype=torch.uint8)
|
||||
for preview in previews_ubyte:
|
||||
i = Image.fromarray(preview.numpy())
|
||||
message = io.BytesIO()
|
||||
message.write((1).to_bytes(length=4, byteorder='big')*2)
|
||||
message.write(ind.to_bytes(length=4, byteorder='big'))
|
||||
message.write(struct.pack('16p', serv.last_node_id.encode('ascii')))
|
||||
i.save(message, format="JPEG", quality=95, compress_level=1)
|
||||
#NOTE: send sync already uses call_soon_threadsafe
|
||||
serv.send_sync(server.BinaryEventTypes.PREVIEW_IMAGE,
|
||||
message.getvalue(), serv.client_id)
|
||||
ind = (ind + 1) % leng
|
||||
def decode_latent_to_preview(self, x0):
|
||||
if hasattr(self, 'taesd'):
|
||||
x_sample = self.taesd.decode(x0).movedim(1, 3)
|
||||
return x_sample
|
||||
else:
|
||||
if self.latent_rgb_factors_reshape is not None:
|
||||
x0 = self.latent_rgb_factors_reshape(x0)
|
||||
self.latent_rgb_factors = self.latent_rgb_factors.to(dtype=x0.dtype, device=x0.device)
|
||||
if self.latent_rgb_factors_bias is not None:
|
||||
self.latent_rgb_factors_bias = self.latent_rgb_factors_bias.to(dtype=x0.dtype, device=x0.device)
|
||||
latent_image = F.linear(x0.movedim(1, -1), self.latent_rgb_factors,
|
||||
bias=self.latent_rgb_factors_bias)
|
||||
return latent_image
|
||||
|
||||
@hook(latent_preview, 'get_previewer')
|
||||
def get_latent_video_previewer(device, latent_format, *args, **kwargs):
|
||||
node_id = serv.last_node_id
|
||||
previewer = get_latent_video_previewer.__wrapped__(device, latent_format, *args, **kwargs)
|
||||
try:
|
||||
extra_info = next(serv.prompt_queue.currently_running.values().__iter__()) \
|
||||
[3]['extra_pnginfo']['workflow']['extra']
|
||||
prev_setting = extra_info.get('VHS_latentpreview', False)
|
||||
if extra_info.get('VHS_latentpreviewrate', 0) != 0:
|
||||
rate_setting = extra_info['VHS_latentpreviewrate']
|
||||
else:
|
||||
rate_setting = rates_table.get(latent_format.__class__.__name__, 8)
|
||||
except:
|
||||
#For safety since there's lots of keys, any of which can fail
|
||||
prev_setting = False
|
||||
if not prev_setting or not hasattr(previewer, "decode_latent_to_preview"):
|
||||
return previewer
|
||||
return WrappedPreviewer(previewer, rate_setting)
|
||||
@@ -55,6 +55,8 @@ def images_generator(directory: str, image_load_cap: int = 0, skip_first_images:
|
||||
sizes[i.size] = count +1
|
||||
size = max(sizes.items(), key=lambda x: x[1])[0]
|
||||
yield size[0], size[1], has_alpha
|
||||
if meta_batch is not None:
|
||||
yield min(image_load_cap, len(dir_files)) or len(dir_files)
|
||||
|
||||
iformat = "RGBA" if has_alpha else "RGB"
|
||||
def load_image(file_path):
|
||||
@@ -99,6 +101,7 @@ def load_images(directory: str, image_load_cap: int = 0, skip_first_images: int
|
||||
(width, height, has_alpha) = next(gen)
|
||||
if meta_batch is not None:
|
||||
meta_batch.inputs[unique_id] = (gen, width, height, has_alpha)
|
||||
meta_batch.total_frames = min(meta_batch.total_frames, next(gen))
|
||||
else:
|
||||
gen, width, height, has_alpha = meta_batch.inputs[unique_id]
|
||||
|
||||
@@ -164,7 +167,7 @@ class LoadImagesFromDirectoryPath:
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"directory": ("STRING", {"default": "X://path/to/images", "vhs_path_extensions": []}),
|
||||
"directory": ("STRING", {"placeholder": "X://path/to/images", "vhs_path_extensions": []}),
|
||||
},
|
||||
"optional": {
|
||||
"image_load_cap": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1}),
|
||||
|
||||
@@ -5,28 +5,66 @@ import torch
|
||||
from PIL import Image, ImageOps
|
||||
import cv2
|
||||
import psutil
|
||||
import subprocess
|
||||
import re
|
||||
import time
|
||||
|
||||
import folder_paths
|
||||
from comfy.utils import common_upscale, ProgressBar
|
||||
import nodes
|
||||
from comfy.k_diffusion.utils import FolderOfImages
|
||||
from .logger import logger
|
||||
from .utils import BIGMAX, DIMMAX, calculate_file_hash, get_sorted_dir_files_from_directory, lazy_get_audio, hash_path, validate_path, strip_path
|
||||
from .utils import BIGMAX, DIMMAX, calculate_file_hash, get_sorted_dir_files_from_directory,\
|
||||
lazy_get_audio, hash_path, validate_path, strip_path, try_download_video, \
|
||||
is_url, imageOrLatent, ffmpeg_path, ENCODE_ARGS, floatOrInt
|
||||
|
||||
|
||||
video_extensions = ['webm', 'mp4', 'mkv', 'gif']
|
||||
video_extensions = ['webm', 'mp4', 'mkv', 'gif', 'mov']
|
||||
|
||||
VHSLoadFormats = {
|
||||
'None': {},
|
||||
'AnimateDiff': {'target_rate': 8, 'dim': (8,0,512,512)},
|
||||
'Mochi': {'target_rate': 24, 'dim': (16,0,848,480), 'frames':(6,1)},
|
||||
'LTXV': {'target_rate': 24, 'dim': (32,0,768,512), 'frames':(8,1)},
|
||||
'Hunyuan': {'target_rate': 24, 'dim': (16,0,848,480), 'frames':(4,1)},
|
||||
'Cosmos': {'target_rate': 24, 'dim': (16,0,1280,704), 'frames':(8,1)},
|
||||
'Wan': {'target_rate': 16, 'dim': (8,0,832,480), 'frames':(4,1)},
|
||||
}
|
||||
"""
|
||||
External plugins may add additional formats to nodes.VHSLoadFormats
|
||||
In addition to shorthand options, direct widget names will map a given dict to options.
|
||||
Adding a third arguement to a frames tuple can enable strict checks on number
|
||||
of loaded frames, i.e (8,1,True)
|
||||
"""
|
||||
if not hasattr(nodes, 'VHSLoadFormats'):
|
||||
nodes.VHSLoadFormats = {}
|
||||
|
||||
def get_load_formats():
|
||||
#TODO: check if {**extra_config.VHSLoafFormats, **VHSLoadFormats} has minimum version
|
||||
formats = {}
|
||||
formats.update(nodes.VHSLoadFormats)
|
||||
formats.update(VHSLoadFormats)
|
||||
return (list(formats.keys()),
|
||||
{'default': 'AnimateDiff', 'formats': formats})
|
||||
def get_format(format):
|
||||
if format in VHSLoadFormats:
|
||||
return VHSLoadFormats[format]
|
||||
return nodes.VHSLoadFormats.get(format, {})
|
||||
|
||||
def is_gif(filename) -> bool:
|
||||
file_parts = filename.split('.')
|
||||
return len(file_parts) > 1 and file_parts[-1] == "gif"
|
||||
|
||||
|
||||
def target_size(width, height, force_size, custom_width, custom_height, downscale_ratio=8) -> tuple[int, int]:
|
||||
if force_size == "Disabled":
|
||||
def target_size(width, height, custom_width, custom_height, downscale_ratio=8) -> tuple[int, int]:
|
||||
if downscale_ratio is None:
|
||||
downscale_ratio = 8
|
||||
if custom_width == 0 and custom_height == 0:
|
||||
pass
|
||||
elif force_size == "Custom Width" or force_size.endswith('x?'):
|
||||
elif custom_height == 0:
|
||||
height *= custom_width/width
|
||||
width = custom_width
|
||||
elif force_size == "Custom Height" or force_size.startswith('?x'):
|
||||
elif custom_width == 0:
|
||||
width *= custom_height/height
|
||||
height = custom_height
|
||||
else:
|
||||
@@ -38,10 +76,9 @@ def target_size(width, height, force_size, custom_width, custom_height, downscal
|
||||
|
||||
def cv_frame_generator(video, force_rate, frame_load_cap, skip_first_frames,
|
||||
select_every_nth, meta_batch=None, unique_id=None):
|
||||
video_cap = cv2.VideoCapture(strip_path(video))
|
||||
if not video_cap.isOpened():
|
||||
video_cap = cv2.VideoCapture(video)
|
||||
if not video_cap.isOpened() or not video_cap.grab():
|
||||
raise ValueError(f"{video} could not be loaded with cv.")
|
||||
pbar = ProgressBar(frame_load_cap) if frame_load_cap > 0 else None
|
||||
|
||||
# extract video metadata
|
||||
fps = video_cap.get(cv2.CAP_PROP_FPS)
|
||||
@@ -50,6 +87,12 @@ def cv_frame_generator(video, force_rate, frame_load_cap, skip_first_frames,
|
||||
total_frames = int(video_cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
duration = total_frames / fps
|
||||
|
||||
width = 0
|
||||
|
||||
if width <=0 or height <=0:
|
||||
_, frame = video_cap.retrieve()
|
||||
height, width, _ = frame.shape
|
||||
|
||||
# set video_cap to look at start_index frame
|
||||
total_frame_count = 0
|
||||
total_frames_evaluated = -1
|
||||
@@ -62,9 +105,20 @@ def cv_frame_generator(video, force_rate, frame_load_cap, skip_first_frames,
|
||||
else:
|
||||
target_frame_time = 1/force_rate
|
||||
|
||||
yield (width, height, fps, duration, total_frames, target_frame_time)
|
||||
|
||||
time_offset=target_frame_time - base_frame_time
|
||||
if total_frames > 0:
|
||||
if force_rate != 0:
|
||||
yieldable_frames = int(total_frames / fps * force_rate)
|
||||
else:
|
||||
yieldable_frames = total_frames
|
||||
if select_every_nth:
|
||||
yieldable_frames //= select_every_nth
|
||||
if frame_load_cap != 0:
|
||||
yieldable_frames = min(frame_load_cap, yieldable_frames)
|
||||
else:
|
||||
yieldable_frames = 0
|
||||
yield (width, height, fps, duration, total_frames, target_frame_time, yieldable_frames)
|
||||
pbar = ProgressBar(yieldable_frames)
|
||||
time_offset=target_frame_time
|
||||
while video_cap.isOpened():
|
||||
if time_offset < target_frame_time:
|
||||
is_returned = video_cap.grab()
|
||||
@@ -103,7 +157,7 @@ def cv_frame_generator(video, force_rate, frame_load_cap, skip_first_frames,
|
||||
prev_frame = frame
|
||||
frames_added += 1
|
||||
if pbar is not None:
|
||||
pbar.update_absolute(frames_added, frame_load_cap)
|
||||
pbar.update_absolute(frames_added, yieldable_frames)
|
||||
# if cap exists and we've reached it, stop processing frames
|
||||
if frame_load_cap > 0 and frames_added >= frame_load_cap:
|
||||
break
|
||||
@@ -113,6 +167,121 @@ def cv_frame_generator(video, force_rate, frame_load_cap, skip_first_frames,
|
||||
if prev_frame is not None:
|
||||
yield prev_frame
|
||||
|
||||
def ffmpeg_frame_generator(video, force_rate, frame_load_cap, start_time,
|
||||
custom_width, custom_height, downscale_ratio=8,
|
||||
meta_batch=None, unique_id=None):
|
||||
args_input = ["-i", video]
|
||||
args_dummy = [ffmpeg_path] + args_input +['-c', 'copy', '-frames:v', '1', "-f", "null", "-"]
|
||||
size_base = None
|
||||
fps_base = None
|
||||
try:
|
||||
dummy_res = subprocess.run(args_dummy, stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise Exception("An error occurred in the ffmpeg subprocess:\n" \
|
||||
+ e.stderr.decode(*ENCODE_ARGS))
|
||||
lines = dummy_res.stderr.decode(*ENCODE_ARGS)
|
||||
if "Video: vp9 " in lines:
|
||||
args_input = ["-c:v", "libvpx-vp9"] + args_input
|
||||
args_dummy = [ffmpeg_path] + args_input +['-c', 'copy', '-frames:v', '1', "-f", "null", "-"]
|
||||
try:
|
||||
dummy_res = subprocess.run(args_dummy, stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise Exception("An error occurred in the ffmpeg subprocess:\n" \
|
||||
+ e.stderr.decode(*ENCODE_ARGS))
|
||||
lines = dummy_res.stderr.decode(*ENCODE_ARGS)
|
||||
|
||||
for line in lines.split('\n'):
|
||||
match = re.search("^ *Stream .* Video.*, ([1-9]|\\d{2,})x(\\d+)", line)
|
||||
if match is not None:
|
||||
size_base = [int(match.group(1)), int(match.group(2))]
|
||||
fps_match = re.search(", ([\\d\\.]+) fps", line)
|
||||
if fps_match:
|
||||
fps_base = float(fps_match.group(1))
|
||||
else:
|
||||
fps_base = 1
|
||||
alpha = re.search("(yuva|rgba|bgra|gbra)", line) is not None
|
||||
break
|
||||
else:
|
||||
raise Exception("Failed to parse video/image information. FFMPEG output:\n" + lines)
|
||||
|
||||
durs_match = re.search("Duration: (\\d+:\\d+:\\d+\\.\\d+),", lines)
|
||||
if durs_match:
|
||||
durs = durs_match.group(1).split(':')
|
||||
duration = int(durs[0])*3600 + int(durs[1])*60 + float(durs[2])
|
||||
else:
|
||||
duration = 0
|
||||
|
||||
if start_time > 0:
|
||||
if start_time > 4:
|
||||
post_seek = ['-ss', '4']
|
||||
args_input = ['-ss', str(start_time - 4)] + args_input
|
||||
else:
|
||||
post_seek = ['-ss', str(start_time)]
|
||||
else:
|
||||
post_seek = []
|
||||
args_all_frames = [ffmpeg_path, "-v", "error", "-an"] + \
|
||||
args_input + ["-pix_fmt", "rgba64le"] + post_seek
|
||||
|
||||
vfilters = []
|
||||
if force_rate != 0:
|
||||
vfilters.append("fps=fps="+str(force_rate))
|
||||
if custom_width != 0 or custom_height != 0:
|
||||
size = target_size(size_base[0], size_base[1], custom_width,
|
||||
custom_height, downscale_ratio=downscale_ratio)
|
||||
ar = float(size[0])/float(size[1])
|
||||
if abs(size_base[0]*ar-size_base[1]) >= 1:
|
||||
#Aspect ratio is changed. Crop to new aspect ratio before scale
|
||||
vfilters.append(f"crop=if(gt({ar}\\,a)\\,iw\\,ih*{ar}):if(gt({ar}\\,a)\\,iw/{ar}\\,ih)")
|
||||
size_arg = ':'.join(map(str,size))
|
||||
vfilters.append(f"scale={size_arg}")
|
||||
else:
|
||||
size = size_base
|
||||
if len(vfilters) > 0:
|
||||
args_all_frames += ["-vf", ",".join(vfilters)]
|
||||
yieldable_frames = (force_rate or fps_base)*duration
|
||||
if frame_load_cap > 0:
|
||||
args_all_frames += ["-frames:v", str(frame_load_cap)]
|
||||
yieldable_frames = min(yieldable_frames, frame_load_cap)
|
||||
yield (size_base[0], size_base[1], fps_base, duration, fps_base * duration,
|
||||
1/(force_rate or fps_base), yieldable_frames, size[0], size[1], alpha)
|
||||
|
||||
args_all_frames += ["-f", "rawvideo", "-"]
|
||||
pbar = ProgressBar(yieldable_frames)
|
||||
try:
|
||||
with subprocess.Popen(args_all_frames, stdout=subprocess.PIPE) as proc:
|
||||
#Manually buffer enough bytes for an image
|
||||
bpi = size[0] * size[1] * 8
|
||||
current_bytes = bytearray(bpi)
|
||||
current_offset=0
|
||||
prev_frame = None
|
||||
while True:
|
||||
bytes_read = proc.stdout.read(bpi - current_offset)
|
||||
if bytes_read is None:#sleep to wait for more data
|
||||
time.sleep(.1)
|
||||
continue
|
||||
if len(bytes_read) == 0:#EOF
|
||||
break
|
||||
current_bytes[current_offset:len(bytes_read)] = bytes_read
|
||||
current_offset+=len(bytes_read)
|
||||
if current_offset == bpi:
|
||||
if prev_frame is not None:
|
||||
yield prev_frame
|
||||
pbar.update(1)
|
||||
prev_frame = np.frombuffer(current_bytes, dtype=np.dtype(np.uint16).newbyteorder("<")).reshape(size[1], size[0], 4) / (2**16-1)
|
||||
if not alpha:
|
||||
prev_frame = prev_frame[:, :, :-1]
|
||||
current_offset = 0
|
||||
except BrokenPipeError as e:
|
||||
raise Exception("An error occured in the ffmpeg subprocess:\n" \
|
||||
+ proc.stderr.read().decode(*ENCODE_ARGS))
|
||||
if meta_batch is not None:
|
||||
meta_batch.inputs.pop(unique_id)
|
||||
meta_batch.has_closed_inputs = True
|
||||
if prev_frame is not None:
|
||||
yield prev_frame
|
||||
|
||||
#Python 3.12 adds an itertools.batched, but it's easily replicated for legacy support
|
||||
def batched(it, n):
|
||||
while batch := tuple(itertools.islice(it, n)):
|
||||
@@ -121,21 +290,50 @@ def batched_vae_encode(images, vae, frames_per_batch):
|
||||
for batch in batched(images, frames_per_batch):
|
||||
image_batch = torch.from_numpy(np.array(batch))
|
||||
yield from vae.encode(image_batch).numpy()
|
||||
def resized_cv_frame_gen(custom_width, custom_height, downscale_ratio, **kwargs):
|
||||
gen = cv_frame_generator(**kwargs)
|
||||
info = next(gen)
|
||||
width, height = info[0], info[1]
|
||||
frames_per_batch = (1920 * 1080 * 16) // (width * height) or 1
|
||||
if kwargs.get('meta_batch', None) is not None:
|
||||
frames_per_batch = min(frames_per_batch, kwargs['meta_batch'].frames_per_batch)
|
||||
if custom_width != 0 or custom_height != 0 or downscale_ratio is not None:
|
||||
new_size = target_size(width, height, custom_width, custom_height, downscale_ratio)
|
||||
yield (*info, new_size[0], new_size[1], False)
|
||||
if new_size[0] != width or new_size[1] != height:
|
||||
def rescale(frame):
|
||||
s = torch.from_numpy(np.fromiter(frame, np.dtype((np.float32, (height, width, 3)))))
|
||||
s = s.movedim(-1,1)
|
||||
s = common_upscale(s, new_size[0], new_size[1], "lanczos", "center")
|
||||
return s.movedim(1,-1).numpy()
|
||||
yield from itertools.chain.from_iterable(map(rescale, batched(gen, frames_per_batch)))
|
||||
return
|
||||
else:
|
||||
yield (*info, info[0], info[1], False)
|
||||
yield from gen
|
||||
|
||||
def load_video_cv(video: str, force_rate: int, force_size: str,
|
||||
custom_width: int,custom_height: int, frame_load_cap: int,
|
||||
skip_first_frames: int, select_every_nth: int,
|
||||
meta_batch=None, unique_id=None, memory_limit_mb=None, vae=None):
|
||||
def load_video(meta_batch=None, unique_id=None, memory_limit_mb=None, vae=None,
|
||||
generator=resized_cv_frame_gen, format='None', **kwargs):
|
||||
if 'force_size' in kwargs:
|
||||
kwargs.pop('force_size')
|
||||
logger.warn("force_size has been removed. Did you reload the webpage after updating?")
|
||||
format = get_format(format)
|
||||
kwargs['video'] = strip_path(kwargs['video'])
|
||||
if vae is not None:
|
||||
downscale_ratio = getattr(vae, "downscale_ratio", 8)
|
||||
else:
|
||||
downscale_ratio = format.get('dim', (1,))[0]
|
||||
if meta_batch is None or unique_id not in meta_batch.inputs:
|
||||
gen = cv_frame_generator(video, force_rate, frame_load_cap, skip_first_frames,
|
||||
select_every_nth, meta_batch, unique_id)
|
||||
(width, height, fps, duration, total_frames, target_frame_time) = next(gen)
|
||||
gen = generator(meta_batch=meta_batch, unique_id=unique_id, downscale_ratio=downscale_ratio, **kwargs)
|
||||
(width, height, fps, duration, total_frames, target_frame_time, yieldable_frames, new_width, new_height, alpha) = next(gen)
|
||||
|
||||
if meta_batch is not None:
|
||||
meta_batch.inputs[unique_id] = (gen, width, height, fps, duration, total_frames, target_frame_time)
|
||||
meta_batch.inputs[unique_id] = (gen, width, height, fps, duration, total_frames, target_frame_time, yieldable_frames, new_width, new_height, alpha)
|
||||
if yieldable_frames:
|
||||
meta_batch.total_frames = min(meta_batch.total_frames, yieldable_frames)
|
||||
|
||||
else:
|
||||
(gen, width, height, fps, duration, total_frames, target_frame_time) = meta_batch.inputs[unique_id]
|
||||
(gen, width, height, fps, duration, total_frames, target_frame_time, yieldable_frames, new_width, new_height, alpha) = meta_batch.inputs[unique_id]
|
||||
|
||||
memory_limit = None
|
||||
if memory_limit_mb is not None:
|
||||
@@ -147,40 +345,37 @@ def load_video_cv(video: str, force_rate: int, force_size: str,
|
||||
memory_limit = (psutil.virtual_memory().available + psutil.swap_memory().free) - 2 ** 27
|
||||
except:
|
||||
logger.warn("Failed to calculate available memory. Memory load limit has been disabled")
|
||||
#space required to load as f32, exist as latent with wiggle room, decode to f32
|
||||
#TODO: fix when vae is not None
|
||||
memory_limit = BIGMAX
|
||||
if vae is not None:
|
||||
#space required to load as f32, exist as latent with wiggle room, decode to f32
|
||||
max_loadable_frames = int(memory_limit//(width*height*3*(4+4+1/10)))
|
||||
else:
|
||||
#TODO: use better estimate for when vae is not None
|
||||
#Consider completely ignoring for load_latent case?
|
||||
max_loadable_frames = int(memory_limit//(width*height*3*(.1)))
|
||||
if meta_batch is not None:
|
||||
if 'frames' in format:
|
||||
if meta_batch.frames_per_batch % format['frames'][0] != format['frames'][1]:
|
||||
error = (meta_batch.frames_per_batch - format['frames'][1]) % format['frames'][0]
|
||||
suggested = meta_batch.frames_per_batch - error
|
||||
if error > format['frames'][0] / 2:
|
||||
suggested += format['frames'][0]
|
||||
raise RuntimeError(f"The chosen frames per batch is incompatible with the selected format. Try {suggested}")
|
||||
if meta_batch.frames_per_batch > max_loadable_frames:
|
||||
raise RuntimeError(f"Meta Batch set to {meta_batch.frames_per_batch} frames but only {max_loadable_frames} can fit in memory")
|
||||
gen = itertools.islice(gen, meta_batch.frames_per_batch)
|
||||
else:
|
||||
original_gen = gen
|
||||
gen = itertools.islice(gen, max_loadable_frames)
|
||||
downscale_ratio = getattr(vae, "downscale_ratio", 8)
|
||||
frames_per_batch = (1920 * 1080 * 16) // (width * height) or 1
|
||||
if force_size != "Disabled" or vae is not None:
|
||||
new_size = target_size(width, height, force_size, custom_width, custom_height, downscale_ratio)
|
||||
if new_size[0] != width or new_size[1] != height:
|
||||
def rescale(frame):
|
||||
s = torch.from_numpy(np.fromiter(frame, np.dtype((np.float32, (height, width, 3)))))
|
||||
s = s.movedim(-1,1)
|
||||
s = common_upscale(s, new_size[0], new_size[1], "lanczos", "center")
|
||||
return s.movedim(1,-1).numpy()
|
||||
gen = itertools.chain.from_iterable(map(rescale, batched(gen, frames_per_batch)))
|
||||
else:
|
||||
new_size = width, height
|
||||
if vae is not None:
|
||||
gen = batched_vae_encode(gen, vae, frames_per_batch)
|
||||
vw,vh = new_size[0]//downscale_ratio, new_size[1]//downscale_ratio
|
||||
images = torch.from_numpy(np.fromiter(gen, np.dtype((np.float32, (4,vh,vw)))))
|
||||
vw,vh = new_width//downscale_ratio, new_height//downscale_ratio
|
||||
channels = getattr(vae, 'latent_channels', 4)
|
||||
images = torch.from_numpy(np.fromiter(gen, np.dtype((np.float32, (channels,vh,vw)))))
|
||||
else:
|
||||
#Some minor wizardry to eliminate a copy and reduce max memory by a factor of ~2
|
||||
images = torch.from_numpy(np.fromiter(gen, np.dtype((np.float32, (new_size[1], new_size[0], 3)))))
|
||||
images = torch.from_numpy(np.fromiter(gen, np.dtype((np.float32, (new_height, new_width, 4 if alpha else 3)))))
|
||||
if meta_batch is None and memory_limit is not None:
|
||||
try:
|
||||
next(original_gen)
|
||||
@@ -189,12 +384,23 @@ def load_video_cv(video: str, force_rate: int, force_size: str,
|
||||
pass
|
||||
if len(images) == 0:
|
||||
raise RuntimeError("No frames generated")
|
||||
|
||||
if 'frames' in format and len(images) % format['frames'][0] != format['frames'][1]:
|
||||
err_msg = f"The number of frames loaded {len(images)}, does not match the requirements of the currently selected format."
|
||||
if len(format['frames']) > 2 and format['frames'][2]:
|
||||
raise RuntimeError(err_msg)
|
||||
div, mod = format['frames'][:2]
|
||||
frames = (len(images) - mod) // div * div + mod
|
||||
images = images[:frames]
|
||||
#Commenting out log message since it's displayed in UI. consider further
|
||||
#logger.warn(err_msg + f" Output has been truncated to {len(images)} frames.")
|
||||
if 'start_time' in kwargs:
|
||||
start_time = kwargs['start_time']
|
||||
else:
|
||||
start_time = kwargs['skip_first_frames'] * target_frame_time
|
||||
target_frame_time *= kwargs.get('select_every_nth', 1)
|
||||
#Setup lambda for lazy audio capture
|
||||
audio = lazy_get_audio(video, skip_first_frames * target_frame_time,
|
||||
frame_load_cap*target_frame_time*select_every_nth)
|
||||
audio = lazy_get_audio(kwargs['video'], start_time, kwargs['frame_load_cap']*target_frame_time)
|
||||
#Adjust target_frame_time for select_every_nth
|
||||
target_frame_time *= select_every_nth
|
||||
video_info = {
|
||||
"source_fps": fps,
|
||||
"source_frame_count": total_frames,
|
||||
@@ -204,13 +410,13 @@ def load_video_cv(video: str, force_rate: int, force_size: str,
|
||||
"loaded_fps": 1/target_frame_time,
|
||||
"loaded_frame_count": len(images),
|
||||
"loaded_duration": len(images) * target_frame_time,
|
||||
"loaded_width": new_size[0],
|
||||
"loaded_height": new_size[1],
|
||||
"loaded_width": new_width,
|
||||
"loaded_height": new_height,
|
||||
}
|
||||
if vae is None:
|
||||
return (images, len(images), audio, video_info, None)
|
||||
return (images, len(images), audio, video_info)
|
||||
else:
|
||||
return (None, len(images), audio, video_info, {"samples": images})
|
||||
return ({"samples": images}, len(images), audio, video_info)
|
||||
|
||||
|
||||
|
||||
@@ -222,37 +428,38 @@ class LoadVideoUpload:
|
||||
for f in os.listdir(input_dir):
|
||||
if os.path.isfile(os.path.join(input_dir, f)):
|
||||
file_parts = f.split('.')
|
||||
if len(file_parts) > 1 and (file_parts[-1] in video_extensions):
|
||||
if len(file_parts) > 1 and (file_parts[-1].lower() in video_extensions):
|
||||
files.append(f)
|
||||
return {"required": {
|
||||
"video": (sorted(files),),
|
||||
"force_rate": ("INT", {"default": 0, "min": 0, "max": 60, "step": 1}),
|
||||
"force_size": (["Disabled", "Custom Height", "Custom Width", "Custom", "256x?", "?x256", "256x256", "512x?", "?x512", "512x512"],),
|
||||
"custom_width": ("INT", {"default": 512, "min": 0, "max": DIMMAX, "step": 8}),
|
||||
"custom_height": ("INT", {"default": 512, "min": 0, "max": DIMMAX, "step": 8}),
|
||||
"frame_load_cap": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1}),
|
||||
"skip_first_frames": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1}),
|
||||
"select_every_nth": ("INT", {"default": 1, "min": 1, "max": BIGMAX, "step": 1}),
|
||||
},
|
||||
"force_rate": (floatOrInt, {"default": 0, "min": 0, "max": 60, "step": 1, "disable": 0}),
|
||||
"custom_width": ("INT", {"default": 0, "min": 0, "max": DIMMAX, 'disable': 0}),
|
||||
"custom_height": ("INT", {"default": 0, "min": 0, "max": DIMMAX, 'disable': 0}),
|
||||
"frame_load_cap": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1, "disable": 0}),
|
||||
"skip_first_frames": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1}),
|
||||
"select_every_nth": ("INT", {"default": 1, "min": 1, "max": BIGMAX, "step": 1}),
|
||||
},
|
||||
"optional": {
|
||||
"meta_batch": ("VHS_BatchManager",),
|
||||
"vae": ("VAE",),
|
||||
"format": get_load_formats(),
|
||||
},
|
||||
"hidden": {
|
||||
"force_size": "STRING",
|
||||
"unique_id": "UNIQUE_ID"
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = "Video Helper Suite 🎥🅥🅗🅢"
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "INT", "AUDIO", "VHS_VIDEOINFO", "LATENT")
|
||||
RETURN_NAMES = ("IMAGE", "frame_count", "audio", "video_info", "LATENT")
|
||||
RETURN_TYPES = (imageOrLatent, "INT", "AUDIO", "VHS_VIDEOINFO")
|
||||
RETURN_NAMES = ("IMAGE", "frame_count", "audio", "video_info")
|
||||
|
||||
FUNCTION = "load_video"
|
||||
|
||||
def load_video(self, **kwargs):
|
||||
kwargs['video'] = folder_paths.get_annotated_filepath(strip_path(kwargs['video']))
|
||||
return load_video_cv(**kwargs)
|
||||
return load_video(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(s, video, **kwargs):
|
||||
@@ -260,7 +467,7 @@ class LoadVideoUpload:
|
||||
return calculate_file_hash(image_path)
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(s, video, force_size, **kwargs):
|
||||
def VALIDATE_INPUTS(s, video):
|
||||
if not folder_paths.exists_annotated_filepath(video):
|
||||
return "Invalid video file: {}".format(video)
|
||||
return True
|
||||
@@ -271,40 +478,194 @@ class LoadVideoPath:
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"video": ("STRING", {"default": "X://insert/path/here.mp4", "vhs_path_extensions": video_extensions}),
|
||||
"force_rate": ("INT", {"default": 0, "min": 0, "max": 60, "step": 1}),
|
||||
"force_size": (["Disabled", "Custom Height", "Custom Width", "Custom", "256x?", "?x256", "256x256", "512x?", "?x512", "512x512"],),
|
||||
"custom_width": ("INT", {"default": 512, "min": 0, "max": DIMMAX, "step": 8}),
|
||||
"custom_height": ("INT", {"default": 512, "min": 0, "max": DIMMAX, "step": 8}),
|
||||
"frame_load_cap": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1}),
|
||||
"video": ("STRING", {"placeholder": "X://insert/path/here.mp4", "vhs_path_extensions": video_extensions}),
|
||||
"force_rate": (floatOrInt, {"default": 0, "min": 0, "max": 60, "step": 1, "disable": 0}),
|
||||
"custom_width": ("INT", {"default": 0, "min": 0, "max": DIMMAX, 'disable': 0}),
|
||||
"custom_height": ("INT", {"default": 0, "min": 0, "max": DIMMAX, 'disable': 0}),
|
||||
"frame_load_cap": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1, "disable": 0}),
|
||||
"skip_first_frames": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1}),
|
||||
"select_every_nth": ("INT", {"default": 1, "min": 1, "max": BIGMAX, "step": 1}),
|
||||
},
|
||||
"optional": {
|
||||
"meta_batch": ("VHS_BatchManager",),
|
||||
"vae": ("VAE",),
|
||||
"format": get_load_formats(),
|
||||
},
|
||||
"hidden": {
|
||||
"force_size": "STRING",
|
||||
"unique_id": "UNIQUE_ID"
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = "Video Helper Suite 🎥🅥🅗🅢"
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "INT", "AUDIO", "VHS_VIDEOINFO", "LATENT")
|
||||
RETURN_NAMES = ("IMAGE", "frame_count", "audio", "video_info", "LATENT")
|
||||
RETURN_TYPES = (imageOrLatent, "INT", "AUDIO", "VHS_VIDEOINFO")
|
||||
RETURN_NAMES = ("IMAGE", "frame_count", "audio", "video_info")
|
||||
|
||||
FUNCTION = "load_video"
|
||||
|
||||
def load_video(self, **kwargs):
|
||||
if kwargs['video'] is None or validate_path(kwargs['video']) != True:
|
||||
raise Exception("video is not a valid path: " + kwargs['video'])
|
||||
return load_video_cv(**kwargs)
|
||||
if is_url(kwargs['video']):
|
||||
kwargs['video'] = try_download_video(kwargs['video']) or kwargs['video']
|
||||
return load_video(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(s, video, **kwargs):
|
||||
return hash_path(video)
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(s, video, **kwargs):
|
||||
def VALIDATE_INPUTS(s, video):
|
||||
return validate_path(video, allow_none=True)
|
||||
|
||||
class LoadVideoFFmpegUpload:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
input_dir = folder_paths.get_input_directory()
|
||||
files = []
|
||||
for f in os.listdir(input_dir):
|
||||
if os.path.isfile(os.path.join(input_dir, f)):
|
||||
file_parts = f.split('.')
|
||||
if len(file_parts) > 1 and (file_parts[-1].lower() in video_extensions):
|
||||
files.append(f)
|
||||
return {"required": {
|
||||
"video": (sorted(files),),
|
||||
"force_rate": (floatOrInt, {"default": 0, "min": 0, "max": 60, "step": 1, "disable": 0}),
|
||||
"custom_width": ("INT", {"default": 0, "min": 0, "max": DIMMAX, 'disable': 0}),
|
||||
"custom_height": ("INT", {"default": 0, "min": 0, "max": DIMMAX, 'disable': 0}),
|
||||
"frame_load_cap": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1, "disable": 0}),
|
||||
"start_time": ("FLOAT", {"default": 0, "min": 0, "max": BIGMAX, "step": .001, "widgetType": "VHSTIMESTAMP"}),
|
||||
},
|
||||
"optional": {
|
||||
"meta_batch": ("VHS_BatchManager",),
|
||||
"vae": ("VAE",),
|
||||
"format": get_load_formats(),
|
||||
},
|
||||
"hidden": {
|
||||
"force_size": "STRING",
|
||||
"unique_id": "UNIQUE_ID"
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = "Video Helper Suite 🎥🅥🅗🅢"
|
||||
|
||||
RETURN_TYPES = (imageOrLatent, "MASK", "AUDIO", "VHS_VIDEOINFO")
|
||||
RETURN_NAMES = ("IMAGE", "mask", "audio", "video_info")
|
||||
|
||||
FUNCTION = "load_video"
|
||||
|
||||
def load_video(self, **kwargs):
|
||||
kwargs['video'] = folder_paths.get_annotated_filepath(strip_path(kwargs['video']))
|
||||
image, _, audio, video_info = load_video(**kwargs, generator=ffmpeg_frame_generator)
|
||||
if image.size(3) == 4:
|
||||
return (image[:,:,:,:3], 1-image[:,:,:,3], audio, video_info)
|
||||
return (image, torch.zeros(image.size(0), 64, 64, device="cpu"), audio, video_info)
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(s, video, **kwargs):
|
||||
image_path = folder_paths.get_annotated_filepath(video)
|
||||
return calculate_file_hash(image_path)
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(s, video):
|
||||
if not folder_paths.exists_annotated_filepath(video):
|
||||
return "Invalid video file: {}".format(video)
|
||||
return True
|
||||
|
||||
|
||||
class LoadVideoFFmpegPath:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"video": ("STRING", {"placeholder": "X://insert/path/here.mp4", "vhs_path_extensions": video_extensions}),
|
||||
"force_rate": (floatOrInt, {"default": 0, "min": 0, "max": 60, "step": 1, "disable": 0}),
|
||||
"custom_width": ("INT", {"default": 0, "min": 0, "max": DIMMAX, 'disable': 0}),
|
||||
"custom_height": ("INT", {"default": 0, "min": 0, "max": DIMMAX, 'disable': 0}),
|
||||
"frame_load_cap": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1, "disable": 0}),
|
||||
"start_time": ("FLOAT", {"default": 0, "min": 0, "max": BIGMAX, "step": .001, "widgetType": "VHSTIMESTAMP"}),
|
||||
},
|
||||
"optional": {
|
||||
"meta_batch": ("VHS_BatchManager",),
|
||||
"vae": ("VAE",),
|
||||
"format": get_load_formats(),
|
||||
},
|
||||
"hidden": {
|
||||
"force_size": "STRING",
|
||||
"unique_id": "UNIQUE_ID"
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = "Video Helper Suite 🎥🅥🅗🅢"
|
||||
|
||||
RETURN_TYPES = (imageOrLatent, "MASK", "AUDIO", "VHS_VIDEOINFO")
|
||||
RETURN_NAMES = ("IMAGE", "mask", "audio", "video_info")
|
||||
|
||||
FUNCTION = "load_video"
|
||||
|
||||
def load_video(self, **kwargs):
|
||||
if kwargs['video'] is None or validate_path(kwargs['video']) != True:
|
||||
raise Exception("video is not a valid path: " + kwargs['video'])
|
||||
if is_url(kwargs['video']):
|
||||
kwargs['video'] = try_download_video(kwargs['video']) or kwargs['video']
|
||||
image, _, audio, video_info = load_video(**kwargs, generator=ffmpeg_frame_generator)
|
||||
if isinstance(image, dict):
|
||||
return (image, None, audio, video_info)
|
||||
if image.size(3) == 4:
|
||||
return (image[:,:,:,:3], 1-image[:,:,:,3], audio, video_info)
|
||||
return (image, torch.zeros(image.size(0), 64, 64, device="cpu"), audio, video_info)
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(s, video, **kwargs):
|
||||
return hash_path(video)
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(s, video):
|
||||
return validate_path(video, allow_none=True)
|
||||
|
||||
class LoadImagePath:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("STRING", {"placeholder": "X://insert/path/here.png", "vhs_path_extensions": list(FolderOfImages.IMG_EXTENSIONS)}),
|
||||
"custom_width": ("INT", {"default": 0, "min": 0, "max": DIMMAX, "step": 8, 'disable': 0}),
|
||||
"custom_height": ("INT", {"default": 0, "min": 0, "max": DIMMAX, "step": 8, 'disable': 0}),
|
||||
},
|
||||
"optional": {
|
||||
"vae": ("VAE",),
|
||||
},
|
||||
"hidden": {
|
||||
"force_size": "STRING",
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = "Video Helper Suite 🎥🅥🅗🅢"
|
||||
|
||||
RETURN_TYPES = (imageOrLatent, "MASK")
|
||||
RETURN_NAMES = ("IMAGE", "mask")
|
||||
|
||||
FUNCTION = "load_image"
|
||||
|
||||
def load_image(self, **kwargs):
|
||||
if kwargs['image'] is None or validate_path(kwargs['image']) != True:
|
||||
raise Exception("image is not a valid path: " + kwargs['image'])
|
||||
kwargs.update({'video': kwargs['image'], 'force_rate': 0, 'frame_load_cap': 0,
|
||||
'start_time': 0})
|
||||
kwargs.pop('image')
|
||||
image, _, _, _ = load_video(**kwargs, generator=ffmpeg_frame_generator)
|
||||
if isinstance(image, dict):
|
||||
return (image, None)
|
||||
if image.size(3) == 4:
|
||||
return (image[:,:,:,:3], 1-image[:,:,:,3])
|
||||
return (image, torch.zeros(image.size(0), 64, 64, device="cpu"))
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(s, image, **kwargs):
|
||||
return hash_path(image)
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(s, image):
|
||||
return validate_path(image, allow_none=True)
|
||||
|
||||
+374
-187
@@ -12,88 +12,117 @@ from PIL.PngImagePlugin import PngInfo
|
||||
from pathlib import Path
|
||||
from string import Template
|
||||
import itertools
|
||||
import functools
|
||||
|
||||
import folder_paths
|
||||
from .logger import logger
|
||||
from .image_latent_nodes import *
|
||||
from .load_video_nodes import LoadVideoUpload, LoadVideoPath
|
||||
from .load_video_nodes import LoadVideoUpload, LoadVideoPath, LoadVideoFFmpegUpload, LoadVideoFFmpegPath, LoadImagePath
|
||||
from .load_images_nodes import LoadImagesFromDirectoryUpload, LoadImagesFromDirectoryPath
|
||||
from .batched_nodes import VAEEncodeBatched, VAEDecodeBatched
|
||||
from .utils import ffmpeg_path, get_audio, hash_path, validate_path, requeue_workflow, gifski_path, calculate_file_hash, strip_path
|
||||
from .utils import ffmpeg_path, get_audio, hash_path, validate_path, requeue_workflow, \
|
||||
gifski_path, calculate_file_hash, strip_path, try_download_video, is_url, \
|
||||
imageOrLatent, BIGMAX, merge_filter_args, ENCODE_ARGS, floatOrInt, cached, \
|
||||
ContainsAll
|
||||
from comfy.utils import ProgressBar
|
||||
|
||||
folder_paths.folder_names_and_paths["VHS_video_formats"] = (
|
||||
[
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "video_formats"),
|
||||
],
|
||||
[".json"]
|
||||
)
|
||||
if 'VHS_video_formats' not in folder_paths.folder_names_and_paths:
|
||||
folder_paths.folder_names_and_paths["VHS_video_formats"] = ((),{".json"})
|
||||
if len(folder_paths.folder_names_and_paths['VHS_video_formats'][1]) == 0:
|
||||
folder_paths.folder_names_and_paths["VHS_video_formats"][1].add(".json")
|
||||
audio_extensions = ['mp3', 'mp4', 'wav', 'ogg']
|
||||
|
||||
def gen_format_widgets(video_format):
|
||||
for k in video_format:
|
||||
if k.endswith("_pass"):
|
||||
for i in range(len(video_format[k])):
|
||||
if isinstance(video_format[k][i], list):
|
||||
item = [video_format[k][i]]
|
||||
yield item
|
||||
video_format[k][i] = item[0]
|
||||
def flatten_list(l):
|
||||
ret = []
|
||||
for e in l:
|
||||
if isinstance(e, list):
|
||||
ret.extend(e)
|
||||
else:
|
||||
if isinstance(video_format[k], list):
|
||||
item = [video_format[k]]
|
||||
yield item
|
||||
video_format[k] = item[0]
|
||||
ret.append(e)
|
||||
return ret
|
||||
|
||||
def iterate_format(video_format, for_widgets=True):
|
||||
"""Provides an iterator over widgets, or arguments"""
|
||||
def indirector(cont, index):
|
||||
if isinstance(cont[index], list) and (not for_widgets
|
||||
or len(cont[index])> 1 and not isinstance(cont[index][1], dict)):
|
||||
inp = yield cont[index]
|
||||
if inp is not None:
|
||||
cont[index] = inp
|
||||
yield
|
||||
for k in video_format:
|
||||
if k == "extra_widgets":
|
||||
if for_widgets:
|
||||
yield from video_format["extra_widgets"]
|
||||
elif k.endswith("_pass"):
|
||||
for i in range(len(video_format[k])):
|
||||
yield from indirector(video_format[k], i)
|
||||
if not for_widgets:
|
||||
video_format[k] = flatten_list(video_format[k])
|
||||
else:
|
||||
yield from indirector(video_format, k)
|
||||
|
||||
base_formats_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "video_formats")
|
||||
@cached(5)
|
||||
def get_video_formats():
|
||||
formats = []
|
||||
format_files = {}
|
||||
for format_name in folder_paths.get_filename_list("VHS_video_formats"):
|
||||
format_name = format_name[:-5]
|
||||
video_format_path = folder_paths.get_full_path("VHS_video_formats", format_name + ".json")
|
||||
with open(video_format_path, 'r') as stream:
|
||||
format_files[format_name] = folder_paths.get_full_path("VHS_video_formats", format_name)
|
||||
for item in os.scandir(base_formats_dir):
|
||||
if not item.is_file() or not item.name.endswith('.json'):
|
||||
continue
|
||||
format_files[item.name[:-5]] = item.path
|
||||
formats = []
|
||||
format_widgets = {}
|
||||
for format_name, path in format_files.items():
|
||||
with open(path, 'r') as stream:
|
||||
video_format = json.load(stream)
|
||||
if "gifski_pass" in video_format and gifski_path is None:
|
||||
#Skip format
|
||||
continue
|
||||
widgets = [w[0] for w in gen_format_widgets(video_format)]
|
||||
widgets = list(iterate_format(video_format))
|
||||
formats.append("video/" + format_name)
|
||||
if (len(widgets) > 0):
|
||||
formats.append(["video/" + format_name, widgets])
|
||||
else:
|
||||
formats.append("video/" + format_name)
|
||||
return formats
|
||||
|
||||
def get_format_widget_defaults(format_name):
|
||||
video_format_path = folder_paths.get_full_path("VHS_video_formats", format_name + ".json")
|
||||
with open(video_format_path, 'r') as stream:
|
||||
video_format = json.load(stream)
|
||||
results = {}
|
||||
for w in gen_format_widgets(video_format):
|
||||
if len(w[0]) > 2 and 'default' in w[0][2]:
|
||||
default = w[0][2]['default']
|
||||
else:
|
||||
if type(w[0][1]) is list:
|
||||
default = w[0][1][0]
|
||||
else:
|
||||
#NOTE: This doesn't respect max/min, but should be good enough as a fallback to a fallback to a fallback
|
||||
default = {"BOOLEAN": False, "INT": 0, "FLOAT": 0, "STRING": ""}[w[0][1]]
|
||||
results[w[0][0]] = default
|
||||
return results
|
||||
|
||||
format_widgets["video/"+ format_name] = widgets
|
||||
return formats, format_widgets
|
||||
|
||||
def apply_format_widgets(format_name, kwargs):
|
||||
video_format_path = folder_paths.get_full_path("VHS_video_formats", format_name + ".json")
|
||||
if os.path.exists(os.path.join(base_formats_dir, format_name + ".json")):
|
||||
video_format_path = os.path.join(base_formats_dir, format_name + ".json")
|
||||
else:
|
||||
video_format_path = folder_paths.get_full_path("VHS_video_formats", format_name)
|
||||
with open(video_format_path, 'r') as stream:
|
||||
video_format = json.load(stream)
|
||||
for w in gen_format_widgets(video_format):
|
||||
assert(w[0][0] in kwargs)
|
||||
if len(w[0]) > 3:
|
||||
w[0] = Template(w[0][3]).substitute(val=kwargs[w[0][0]])
|
||||
else:
|
||||
w[0] = str(kwargs[w[0][0]])
|
||||
for w in iterate_format(video_format):
|
||||
if w[0] not in kwargs:
|
||||
if len(w) > 2 and 'default' in w[2]:
|
||||
default = w[2]['default']
|
||||
else:
|
||||
if type(w[1]) is list:
|
||||
default = w[1][0]
|
||||
else:
|
||||
#NOTE: This doesn't respect max/min, but should be good enough as a fallback to a fallback to a fallback
|
||||
default = {"BOOLEAN": False, "INT": 0, "FLOAT": 0, "STRING": ""}[w[1]]
|
||||
kwargs[w[0]] = default
|
||||
logger.warn(f"Missing input for {w[0]} has been set to {default}")
|
||||
wit = iterate_format(video_format, False)
|
||||
for w in wit:
|
||||
while isinstance(w, list):
|
||||
if len(w) == 1:
|
||||
#TODO: mapping=kwargs should be safer, but results in key errors, investigate why
|
||||
w = [Template(x).substitute(**kwargs) for x in w[0]]
|
||||
break
|
||||
elif isinstance(w[1], dict):
|
||||
w = w[1][str(kwargs[w[0]])]
|
||||
elif len(w) > 3:
|
||||
w = Template(w[3]).substitute(val=kwargs[w[0]])
|
||||
else:
|
||||
w = str(kwargs[w[0]])
|
||||
wit.send(w)
|
||||
return video_format
|
||||
|
||||
def tensor_to_int(tensor, bits):
|
||||
#TODO: investigate benefit of rounding by adding 0.5 before clip/cast
|
||||
tensor = tensor.cpu().numpy() * (2**bits-1)
|
||||
tensor = tensor.cpu().numpy() * (2**bits-1) + 0.5
|
||||
return np.clip(tensor, 0, (2**bits-1))
|
||||
def tensor_to_shorts(tensor):
|
||||
return tensor_to_int(tensor, 16).astype(np.uint16)
|
||||
@@ -107,19 +136,28 @@ def ffmpeg_process(args, video_format, video_metadata, file_path, env):
|
||||
total_frames_output = 0
|
||||
if video_format.get('save_metadata', 'False') != 'False':
|
||||
os.makedirs(folder_paths.get_temp_directory(), exist_ok=True)
|
||||
metadata = json.dumps(video_metadata)
|
||||
metadata_path = os.path.join(folder_paths.get_temp_directory(), "metadata.txt")
|
||||
#metadata from file should escape = ; # \ and newline
|
||||
metadata = metadata.replace("\\","\\\\")
|
||||
metadata = metadata.replace(";","\\;")
|
||||
metadata = metadata.replace("#","\\#")
|
||||
metadata = metadata.replace("=","\\=")
|
||||
metadata = metadata.replace("\n","\\\n")
|
||||
metadata = "comment=" + metadata
|
||||
def escape_ffmpeg_metadata(key, value):
|
||||
value = str(value)
|
||||
value = value.replace("\\","\\\\")
|
||||
value = value.replace(";","\\;")
|
||||
value = value.replace("#","\\#")
|
||||
value = value.replace("=","\\=")
|
||||
value = value.replace("\n","\\\n")
|
||||
return f"{key}={value}"
|
||||
|
||||
with open(metadata_path, "w") as f:
|
||||
f.write(";FFMETADATA1\n")
|
||||
f.write(metadata)
|
||||
m_args = args[:1] + ["-i", metadata_path] + args[1:] + ["-metadata", "creation_time=now"]
|
||||
if "prompt" in video_metadata:
|
||||
f.write(escape_ffmpeg_metadata("prompt", json.dumps(video_metadata["prompt"])) + "\n")
|
||||
if "workflow" in video_metadata:
|
||||
f.write(escape_ffmpeg_metadata("workflow", json.dumps(video_metadata["workflow"])) + "\n")
|
||||
for k, v in video_metadata.items():
|
||||
if k not in ["prompt", "workflow"]:
|
||||
f.write(escape_ffmpeg_metadata(k, json.dumps(v)) + "\n")
|
||||
|
||||
m_args = args[:1] + ["-i", metadata_path] + args[1:] + ["-metadata", "creation_time=now", "-movflags", "use_metadata_tags"]
|
||||
with subprocess.Popen(m_args + [file_path], stderr=subprocess.PIPE,
|
||||
stdin=subprocess.PIPE, env=env) as proc:
|
||||
try:
|
||||
@@ -138,9 +176,9 @@ def ffmpeg_process(args, video_format, video_metadata, file_path, env):
|
||||
#and seems to never occur concurrent to the metadata issue
|
||||
if os.path.exists(file_path):
|
||||
raise Exception("An error occurred in the ffmpeg subprocess:\n" \
|
||||
+ err.decode("utf-8"))
|
||||
+ err.decode(*ENCODE_ARGS))
|
||||
#Res was not set
|
||||
print(err.decode("utf-8"), end="", file=sys.stderr)
|
||||
print(err.decode(*ENCODE_ARGS), end="", file=sys.stderr)
|
||||
logger.warn("An error occurred when saving with metadata")
|
||||
if res != b'':
|
||||
with subprocess.Popen(args + [file_path], stderr=subprocess.PIPE,
|
||||
@@ -156,17 +194,19 @@ def ffmpeg_process(args, video_format, video_metadata, file_path, env):
|
||||
except BrokenPipeError as e:
|
||||
res = proc.stderr.read()
|
||||
raise Exception("An error occurred in the ffmpeg subprocess:\n" \
|
||||
+ res.decode("utf-8"))
|
||||
+ res.decode(*ENCODE_ARGS))
|
||||
yield total_frames_output
|
||||
if len(res) > 0:
|
||||
print(res.decode("utf-8"), end="", file=sys.stderr)
|
||||
print(res.decode(*ENCODE_ARGS), end="", file=sys.stderr)
|
||||
|
||||
def gifski_process(args, video_format, file_path, env):
|
||||
def gifski_process(args, dimensions, frame_rate, video_format, file_path, env):
|
||||
frame_data = yield
|
||||
with subprocess.Popen(args + video_format['main_pass'] + ['-f', 'yuv4mpegpipe', '-'],
|
||||
stderr=subprocess.PIPE, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, env=env) as procff:
|
||||
with subprocess.Popen([gifski_path] + video_format['gifski_pass']
|
||||
+ ['-W', f'{dimensions[0]}', '-H', f'{dimensions[1]}']
|
||||
+ ['-r', f'{frame_rate}']
|
||||
+ ['-q', '-o', file_path, '-'], stderr=subprocess.PIPE,
|
||||
stdin=procff.stdout, stdout=subprocess.PIPE,
|
||||
env=env) as procgs:
|
||||
@@ -185,14 +225,14 @@ def gifski_process(args, video_format, file_path, env):
|
||||
resgs = procgs.stderr.read()
|
||||
raise Exception("An error occurred while creating gifski output\n" \
|
||||
+ "Make sure you are using gifski --version >=1.32.0\nffmpeg: " \
|
||||
+ resff.decode("utf-8") + '\ngifski: ' + resgs.decode("utf-8"))
|
||||
+ resff.decode(*ENCODE_ARGS) + '\ngifski: ' + resgs.decode(*ENCODE_ARGS))
|
||||
if len(resff) > 0:
|
||||
print(resff.decode("utf-8"), end="", file=sys.stderr)
|
||||
print(resff.decode(*ENCODE_ARGS), end="", file=sys.stderr)
|
||||
if len(resgs) > 0:
|
||||
print(resgs.decode("utf-8"), end="", file=sys.stderr)
|
||||
print(resgs.decode(*ENCODE_ARGS), end="", file=sys.stderr)
|
||||
#should always be empty as the quiet flag is passed
|
||||
if len(outgs) > 0:
|
||||
print(outgs.decode("utf-8"))
|
||||
print(outgs.decode(*ENCODE_ARGS))
|
||||
|
||||
def to_pingpong(inp):
|
||||
if not hasattr(inp, "__getitem__"):
|
||||
@@ -204,31 +244,31 @@ def to_pingpong(inp):
|
||||
class VideoCombine:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
ffmpeg_formats = get_video_formats()
|
||||
ffmpeg_formats, format_widgets = get_video_formats()
|
||||
format_widgets["image/webp"] = [['lossless', "BOOLEAN", {'default': True}]]
|
||||
return {
|
||||
"required": {
|
||||
"images": (imageOrLatent,),
|
||||
"frame_rate": (
|
||||
"FLOAT",
|
||||
floatOrInt,
|
||||
{"default": 8, "min": 1, "step": 1},
|
||||
),
|
||||
"loop_count": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}),
|
||||
"filename_prefix": ("STRING", {"default": "AnimateDiff"}),
|
||||
"format": (["image/gif", "image/webp"] + ffmpeg_formats,),
|
||||
"format": (["image/gif", "image/webp"] + ffmpeg_formats, {'formats': format_widgets}),
|
||||
"pingpong": ("BOOLEAN", {"default": False}),
|
||||
"save_output": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
"optional": {
|
||||
"images": ("IMAGE",),
|
||||
"audio": ("AUDIO",),
|
||||
"meta_batch": ("VHS_BatchManager",),
|
||||
"vae": ("VAE",),
|
||||
"latents": ("LATENT",),
|
||||
},
|
||||
"hidden": {
|
||||
"hidden": ContainsAll({
|
||||
"prompt": "PROMPT",
|
||||
"extra_pnginfo": "EXTRA_PNGINFO",
|
||||
"unique_id": "UNIQUE_ID"
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("VHS_FILENAMES",)
|
||||
@@ -253,7 +293,8 @@ class VideoCombine:
|
||||
unique_id=None,
|
||||
manual_format_widgets=None,
|
||||
meta_batch=None,
|
||||
vae=None
|
||||
vae=None,
|
||||
**kwargs
|
||||
):
|
||||
if latents is not None:
|
||||
images = latents
|
||||
@@ -271,8 +312,8 @@ class VideoCombine:
|
||||
pbar = ProgressBar(num_frames)
|
||||
if vae is not None:
|
||||
downscale_ratio = getattr(vae, "downscale_ratio", 8)
|
||||
width = images.size(3)*downscale_ratio
|
||||
height = images.size(2)*downscale_ratio
|
||||
width = images.size(-1)*downscale_ratio
|
||||
height = images.size(-2)*downscale_ratio
|
||||
frames_per_batch = (1920 * 1080 * 16) // (width * height) or 1
|
||||
#Python 3.12 adds an itertools.batched, but it's easily replicated for legacy support
|
||||
def batched(it, n):
|
||||
@@ -286,6 +327,9 @@ class VideoCombine:
|
||||
first_image = next(images)
|
||||
#repush first_image
|
||||
images = itertools.chain([first_image], images)
|
||||
#A single image has 3 dimensions. Discard higher dimensions
|
||||
while len(first_image.shape) > 3:
|
||||
first_image = first_image[0]
|
||||
else:
|
||||
first_image = images[0]
|
||||
images = iter(images)
|
||||
@@ -313,6 +357,9 @@ class VideoCombine:
|
||||
for x in extra_pnginfo:
|
||||
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
|
||||
video_metadata[x] = extra_pnginfo[x]
|
||||
extra_options = extra_pnginfo.get('workflow', {}).get('extra', {})
|
||||
else:
|
||||
extra_options = {}
|
||||
metadata.add_text("CreationTime", datetime.datetime.now().isoformat(" ")[:19])
|
||||
|
||||
if meta_batch is not None and unique_id in meta_batch.outputs:
|
||||
@@ -338,13 +385,14 @@ class VideoCombine:
|
||||
output_process = None
|
||||
|
||||
# save first frame as png to keep metadata
|
||||
file = f"{filename}_{counter:05}.png"
|
||||
file_path = os.path.join(full_output_folder, file)
|
||||
Image.fromarray(tensor_to_bytes(first_image)).save(
|
||||
file_path,
|
||||
pnginfo=metadata,
|
||||
compress_level=4,
|
||||
)
|
||||
first_image_file = f"{filename}_{counter:05}.png"
|
||||
file_path = os.path.join(full_output_folder, first_image_file)
|
||||
if extra_options.get('VHS_MetadataImage', True) != False:
|
||||
Image.fromarray(tensor_to_bytes(first_image)).save(
|
||||
file_path,
|
||||
pnginfo=metadata,
|
||||
compress_level=4,
|
||||
)
|
||||
output_files.append(file_path)
|
||||
|
||||
format_type, format_ext = format.split("/")
|
||||
@@ -359,11 +407,16 @@ class VideoCombine:
|
||||
exif = Image.Exif()
|
||||
exif[ExifTags.IFD.Exif] = {36867: datetime.datetime.now().isoformat(" ")[:19]}
|
||||
image_kwargs['exif'] = exif
|
||||
image_kwargs['lossless'] = kwargs.get("lossless", True)
|
||||
file = f"{filename}_{counter:05}.{format_ext}"
|
||||
file_path = os.path.join(full_output_folder, file)
|
||||
if pingpong:
|
||||
images = to_pingpong(images)
|
||||
frames = map(lambda x : Image.fromarray(tensor_to_bytes(x)), images)
|
||||
def frames_gen(images):
|
||||
for i in images:
|
||||
pbar.update(1)
|
||||
yield Image.fromarray(tensor_to_bytes(i))
|
||||
frames = frames_gen(images)
|
||||
# Use pillow directly to save an animated image
|
||||
next(frames).save(
|
||||
file_path,
|
||||
@@ -381,27 +434,14 @@ class VideoCombine:
|
||||
if ffmpeg_path is None:
|
||||
raise ProcessLookupError(f"ffmpeg is required for video outputs and could not be found.\nIn order to use video outputs, you must either:\n- Install imageio-ffmpeg with pip,\n- Place a ffmpeg executable in {os.path.abspath('')}, or\n- Install ffmpeg and add it to the system path.")
|
||||
|
||||
#Acquire additional format_widget values
|
||||
kwargs = None
|
||||
if manual_format_widgets is None:
|
||||
if prompt is not None:
|
||||
kwargs = prompt[unique_id]['inputs']
|
||||
else:
|
||||
manual_format_widgets = {}
|
||||
if kwargs is None:
|
||||
kwargs = get_format_widget_defaults(format_ext)
|
||||
missing = {}
|
||||
for k in kwargs.keys():
|
||||
if k in manual_format_widgets:
|
||||
kwargs[k] = manual_format_widgets[k]
|
||||
else:
|
||||
missing[k] = kwargs[k]
|
||||
if len(missing) > 0:
|
||||
logger.warn("Extra format values were not provided, the following defaults will be used: " + str(kwargs) + "\nThis is likely due to usage of ComfyUI-to-python. These values can be manually set by supplying a manual_format_widgets argument")
|
||||
if manual_format_widgets is not None:
|
||||
logger.warn("Format args can now be passed directly. The manual_format_widgets argument is now deprecated")
|
||||
kwargs.update(manual_format_widgets)
|
||||
|
||||
video_format = apply_format_widgets(format_ext, kwargs)
|
||||
has_alpha = first_image.shape[-1] == 4
|
||||
dim_alignment = video_format.get("dim_alignment", 8)
|
||||
kwargs["has_alpha"] = has_alpha
|
||||
video_format = apply_format_widgets(format_ext, kwargs)
|
||||
dim_alignment = video_format.get("dim_alignment", 2)
|
||||
if (first_image.shape[1] % dim_alignment) or (first_image.shape[0] % dim_alignment):
|
||||
#output frames must be padded
|
||||
to_pad = (-first_image.shape[1] % dim_alignment,
|
||||
@@ -414,20 +454,22 @@ class VideoCombine:
|
||||
padded = padfunc(image.to(dtype=torch.float32))
|
||||
return padded.permute((1,2,0))
|
||||
images = map(pad, images)
|
||||
new_dims = (-first_image.shape[1] % dim_alignment + first_image.shape[1],
|
||||
-first_image.shape[0] % dim_alignment + first_image.shape[0])
|
||||
dimensions = f"{new_dims[0]}x{new_dims[1]}"
|
||||
dimensions = (-first_image.shape[1] % dim_alignment + first_image.shape[1],
|
||||
-first_image.shape[0] % dim_alignment + first_image.shape[0])
|
||||
logger.warn("Output images were not of valid resolution and have had padding applied")
|
||||
else:
|
||||
dimensions = f"{first_image.shape[1]}x{first_image.shape[0]}"
|
||||
if loop_count > 0:
|
||||
loop_args = ["-vf", "loop=loop=" + str(loop_count)+":size=" + str(num_frames)]
|
||||
else:
|
||||
loop_args = []
|
||||
dimensions = (first_image.shape[1], first_image.shape[0])
|
||||
if pingpong:
|
||||
if meta_batch is not None:
|
||||
logger.error("pingpong is incompatible with batched output")
|
||||
images = to_pingpong(images)
|
||||
if num_frames > 2:
|
||||
num_frames += num_frames -2
|
||||
pbar.total = num_frames
|
||||
if loop_count > 0:
|
||||
loop_args = ["-vf", "loop=loop=" + str(loop_count)+":size=" + str(num_frames)]
|
||||
else:
|
||||
loop_args = []
|
||||
if video_format.get('input_color_depth', '8bit') == '16bit':
|
||||
images = map(tensor_to_shorts, images)
|
||||
if has_alpha:
|
||||
@@ -447,7 +489,18 @@ class VideoCombine:
|
||||
if bitrate is not None:
|
||||
bitrate_arg = ["-b:v", str(bitrate) + "M" if video_format.get('megabit') == 'True' else str(bitrate) + "K"]
|
||||
args = [ffmpeg_path, "-v", "error", "-f", "rawvideo", "-pix_fmt", i_pix_fmt,
|
||||
"-s", dimensions, "-r", str(frame_rate), "-i", "-"] \
|
||||
# The image data is in an undefined generic RGB color space, which in practice means sRGB.
|
||||
# sRGB has the same primaries and matrix as BT.709, but a different transfer function (gamma),
|
||||
# called by the sRGB standard name IEC 61966-2-1. However, video hosting platforms like YouTube
|
||||
# standardize on full BT.709 and will convert the colors accordingly. This last minute change
|
||||
# in colors can be confusing to users. We can counter it by lying about the transfer function
|
||||
# on a per format basis, i.e. for video we will lie to FFmpeg that it is already BT.709. Also,
|
||||
# because the input data is in RGB (not YUV) it is more efficient (fewer scale filter invocations)
|
||||
# to specify the input color space as RGB and then later, if the format actually wants YUV,
|
||||
# to convert it to BT.709 YUV via FFmpeg's -vf "scale=out_color_matrix=bt709".
|
||||
"-color_range", "pc", "-colorspace", "rgb", "-color_primaries", "bt709",
|
||||
"-color_trc", video_format.get("fake_trc", "iec61966-2-1"),
|
||||
"-s", f"{dimensions[0]}x{dimensions[1]}", "-r", str(frame_rate), "-i", "-"] \
|
||||
+ loop_args
|
||||
|
||||
images = map(lambda x: x.tobytes(), images)
|
||||
@@ -464,21 +517,27 @@ class VideoCombine:
|
||||
raise Exception("Formats which require a pre_pass are incompatible with Batch Manager.")
|
||||
images = [b''.join(images)]
|
||||
os.makedirs(folder_paths.get_temp_directory(), exist_ok=True)
|
||||
pre_pass_args = args[:13] + video_format['pre_pass']
|
||||
in_args_len = args.index("-i") + 2 # The index after ["-i", "-"]
|
||||
pre_pass_args = args[:in_args_len] + video_format['pre_pass']
|
||||
merge_filter_args(pre_pass_args)
|
||||
try:
|
||||
subprocess.run(pre_pass_args, input=images[0], env=env,
|
||||
capture_output=True, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise Exception("An error occurred in the ffmpeg prepass:\n" \
|
||||
+ e.stderr.decode("utf-8"))
|
||||
+ e.stderr.decode(*ENCODE_ARGS))
|
||||
if "inputs_main_pass" in video_format:
|
||||
args = args[:13] + video_format['inputs_main_pass'] + args[13:]
|
||||
in_args_len = args.index("-i") + 2 # The index after ["-i", "-"]
|
||||
args = args[:in_args_len] + video_format['inputs_main_pass'] + args[in_args_len:]
|
||||
|
||||
if output_process is None:
|
||||
if 'gifski_pass' in video_format:
|
||||
output_process = gifski_process(args, video_format, file_path, env)
|
||||
format = 'image/gif'
|
||||
output_process = gifski_process(args, dimensions, frame_rate, video_format, file_path, env)
|
||||
audio = None
|
||||
else:
|
||||
args += video_format['main_pass'] + bitrate_arg
|
||||
merge_filter_args(args)
|
||||
output_process = ffmpeg_process(args, video_format, video_metadata, file_path, env)
|
||||
#Proceed to first yield
|
||||
output_process.send(None)
|
||||
@@ -508,7 +567,15 @@ class VideoCombine:
|
||||
|
||||
output_files.append(file_path)
|
||||
|
||||
|
||||
a_waveform = None
|
||||
if audio is not None:
|
||||
try:
|
||||
#safely check if audio produced by VHS_LoadVideo actually exists
|
||||
a_waveform = audio['waveform']
|
||||
except:
|
||||
pass
|
||||
if a_waveform is not None:
|
||||
# Create audio file if input was provided
|
||||
output_file_with_audio = f"{filename}_{counter:05}-audio.{video_format['extension']}"
|
||||
output_file_with_audio_path = os.path.join(full_output_folder, output_file_with_audio)
|
||||
@@ -522,44 +589,48 @@ class VideoCombine:
|
||||
#Reconsider forcing apad/shortest
|
||||
channels = audio['waveform'].size(1)
|
||||
min_audio_dur = total_frames_output / frame_rate + 1
|
||||
if video_format.get('trim_to_audio', 'False') != 'False':
|
||||
apad = []
|
||||
else:
|
||||
apad = ["-af", "apad=whole_dur="+str(min_audio_dur)]
|
||||
mux_args = [ffmpeg_path, "-v", "error", "-n", "-i", file_path,
|
||||
"-ar", str(audio['sample_rate']), "-ac", str(channels),
|
||||
"-f", "f32le", "-i", "-", "-c:v", "copy"] \
|
||||
+ video_format["audio_pass"] \
|
||||
+ ["-af", "apad=whole_dur="+str(min_audio_dur),
|
||||
"-shortest", output_file_with_audio_path]
|
||||
+ apad + ["-shortest", output_file_with_audio_path]
|
||||
|
||||
audio_data = audio['waveform'].squeeze(0).transpose(0,1) \
|
||||
.numpy().tobytes()
|
||||
merge_filter_args(mux_args, '-af')
|
||||
try:
|
||||
res = subprocess.run(mux_args, input=audio_data,
|
||||
env=env, capture_output=True, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise Exception("An error occured in the ffmpeg subprocess:\n" \
|
||||
+ e.stderr.decode("utf-8"))
|
||||
+ e.stderr.decode(*ENCODE_ARGS))
|
||||
if res.stderr:
|
||||
print(res.stderr.decode("utf-8"), end="", file=sys.stderr)
|
||||
print(res.stderr.decode(*ENCODE_ARGS), end="", file=sys.stderr)
|
||||
output_files.append(output_file_with_audio_path)
|
||||
#Return this file with audio to the webui.
|
||||
#It will be muted unless opened or saved with right click
|
||||
file = output_file_with_audio
|
||||
|
||||
previews = [
|
||||
{
|
||||
if extra_options.get('VHS_KeepIntermediate', True) == False:
|
||||
for intermediate in output_files[1:-1]:
|
||||
if os.path.exists(intermediate):
|
||||
os.remove(intermediate)
|
||||
preview = {
|
||||
"filename": file,
|
||||
"subfolder": subfolder,
|
||||
"type": "output" if save_output else "temp",
|
||||
"format": format,
|
||||
"frame_rate": frame_rate,
|
||||
"workflow": first_image_file,
|
||||
"fullpath": output_files[-1],
|
||||
}
|
||||
]
|
||||
if num_frames == 1 and 'png' in format and '%03d' in file:
|
||||
previews[0]['format'] = 'image/png'
|
||||
previews[0]['filename'] = file.replace('%03d', '001')
|
||||
return {"ui": {"gifs": previews}, "result": ((save_output, output_files),)}
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(self, format, **kwargs):
|
||||
return True
|
||||
preview['format'] = 'image/png'
|
||||
preview['filename'] = file.replace('%03d', '001')
|
||||
return {"ui": {"gifs": [preview]}, "result": ((save_output, output_files),)}
|
||||
|
||||
class LoadAudio:
|
||||
@classmethod
|
||||
@@ -569,23 +640,30 @@ class LoadAudio:
|
||||
"required": {
|
||||
"audio_file": ("STRING", {"default": "input/", "vhs_path_extensions": ['wav','mp3','ogg','m4a','flac']}),
|
||||
},
|
||||
"optional" : {"seek_seconds": ("FLOAT", {"default": 0, "min": 0})}
|
||||
"optional" : {
|
||||
"seek_seconds": ("FLOAT", {"default": 0, "min": 0, "widgetType": "VHSTIMESTAMP"}),
|
||||
"duration": ("FLOAT" , {"default": 0, "min": 0, "max": 10000000, "step": 0.01, "widgetType": "VHSTIMESTAMP"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("AUDIO",)
|
||||
RETURN_NAMES = ("audio",)
|
||||
RETURN_TYPES = ("AUDIO", "FLOAT")
|
||||
RETURN_NAMES = ("audio", "duration")
|
||||
CATEGORY = "Video Helper Suite 🎥🅥🅗🅢/audio"
|
||||
FUNCTION = "load_audio"
|
||||
def load_audio(self, audio_file, seek_seconds):
|
||||
def load_audio(self, audio_file, seek_seconds=0, duration=0):
|
||||
audio_file = strip_path(audio_file)
|
||||
if audio_file is None or validate_path(audio_file) != True:
|
||||
raise Exception("audio_file is not a valid path: " + audio_file)
|
||||
if is_url(audio_file):
|
||||
audio_file = try_download_video(audio_file) or audio_file
|
||||
#Eagerly fetch the audio since the user must be using it if the
|
||||
#node executes, unlike Load Video
|
||||
return (get_audio(audio_file, start_time=seek_seconds),)
|
||||
audio = get_audio(audio_file, start_time=seek_seconds, duration=duration)
|
||||
loaded_duration = audio['waveform'].size(2)/audio['sample_rate']
|
||||
return (audio, loaded_duration)
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(s, audio_file, seek_seconds):
|
||||
def IS_CHANGED(s, audio_file, **kwargs):
|
||||
return hash_path(audio_file)
|
||||
|
||||
@classmethod
|
||||
@@ -603,27 +681,30 @@ class LoadAudioUpload:
|
||||
if len(file_parts) > 1 and (file_parts[-1] in audio_extensions):
|
||||
files.append(f)
|
||||
return {"required": {
|
||||
"audio": (sorted(files),),
|
||||
"start_time": ("FLOAT" , {"default": 0, "min": 0, "max": 10000000, "step": 0.01}),
|
||||
"duration": ("FLOAT" , {"default": 0, "min": 0, "max": 10000000, "step": 0.01}),
|
||||
"audio": (sorted(files),),},
|
||||
"optional": {
|
||||
"start_time": ("FLOAT" , {"default": 0, "min": 0, "max": 10000000, "step": 0.01, "widgetType": "VHSTIMESTAMP"}),
|
||||
"duration": ("FLOAT" , {"default": 0, "min": 0, "max": 10000000, "step": 0.01, "widgetType": "VHSTIMESTAMP"}),
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = "Video Helper Suite 🎥🅥🅗🅢/audio"
|
||||
|
||||
RETURN_TYPES = ("AUDIO", )
|
||||
RETURN_NAMES = ("audio",)
|
||||
RETURN_TYPES = ("AUDIO", "FLOAT")
|
||||
RETURN_NAMES = ("audio", "duration")
|
||||
FUNCTION = "load_audio"
|
||||
|
||||
def load_audio(self, start_time, duration, **kwargs):
|
||||
def load_audio(self, start_time=0, duration=0, **kwargs):
|
||||
audio_file = folder_paths.get_annotated_filepath(strip_path(kwargs['audio']))
|
||||
if audio_file is None or validate_path(audio_file) != True:
|
||||
raise Exception("audio_file is not a valid path: " + audio_file)
|
||||
|
||||
return (get_audio(audio_file, start_time, duration),)
|
||||
|
||||
audio = get_audio(audio_file, start_time, duration)
|
||||
loaded_duration = audio['waveform'].size(2)/audio['sample_rate']
|
||||
return (audio, loaded_duration)
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(s, audio, start_time, duration):
|
||||
def IS_CHANGED(s, audio, **kwargs):
|
||||
audio_file = folder_paths.get_annotated_filepath(strip_path(audio))
|
||||
return hash_path(audio_file)
|
||||
|
||||
@@ -656,14 +737,45 @@ class AudioToVHSAudio:
|
||||
capture_output=True, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise Exception("An error occured in the ffmpeg subprocess:\n" \
|
||||
+ e.stderr.decode("utf-8"))
|
||||
+ e.stderr.decode(*ENCODE_ARGS))
|
||||
if res.stderr:
|
||||
print(res.stderr.decode("utf-8"), end="", file=sys.stderr)
|
||||
return (lambda x : res.stdout,)
|
||||
|
||||
print(res.stderr.decode(*ENCODE_ARGS), end="", file=sys.stderr)
|
||||
return (lambda: res.stdout,)
|
||||
|
||||
class VHSAudioToAudio:
|
||||
"""Legacy method for external nodes that utilized VHS_AUDIO,
|
||||
VHS_AUDIO is deprecated as a format and should no longer be used"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {"vhs_audio": ("VHS_AUDIO",)}}
|
||||
CATEGORY = "Video Helper Suite 🎥🅥🅗🅢/audio"
|
||||
|
||||
RETURN_TYPES = ("AUDIO", )
|
||||
RETURN_NAMES = ("audio",)
|
||||
FUNCTION = "convert_audio"
|
||||
|
||||
def convert_audio(self, vhs_audio):
|
||||
if not vhs_audio or not vhs_audio():
|
||||
raise Exception("audio input is not valid")
|
||||
args = [ffmpeg_path, "-i", '-']
|
||||
try:
|
||||
res = subprocess.run(args + ["-f", "f32le", "-"], input=vhs_audio(),
|
||||
capture_output=True, check=True)
|
||||
audio = torch.frombuffer(bytearray(res.stdout), dtype=torch.float32)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise Exception("An error occured in the ffmpeg subprocess:\n" \
|
||||
+ e.stderr.decode(*ENCODE_ARGS))
|
||||
match = re.search(', (\\d+) Hz, (\\w+), ',res.stderr.decode(*ENCODE_ARGS))
|
||||
if match:
|
||||
ar = int(match.group(1))
|
||||
#NOTE: Just throwing an error for other channel types right now
|
||||
#Will deal with issues if they come
|
||||
ac = {"mono": 1, "stereo": 2}[match.group(2)]
|
||||
else:
|
||||
ar = 44100
|
||||
ac = 2
|
||||
audio = audio.reshape((-1,ac)).transpose(0,1).unsqueeze(0)
|
||||
return ({'waveform': audio, 'sample_rate': ar},)
|
||||
|
||||
class PruneOutputs:
|
||||
@classmethod
|
||||
@@ -692,7 +804,8 @@ class PruneOutputs:
|
||||
if options in ["All"]:
|
||||
delete_list.append(filenames[1][-1])
|
||||
|
||||
output_dirs = [os.path.abspath("output"), os.path.abspath("temp")]
|
||||
output_dirs = [folder_paths.get_output_directory(),
|
||||
folder_paths.get_temp_directory()]
|
||||
for file in delete_list:
|
||||
#Check that path is actually an output directory
|
||||
if (os.path.commonpath([output_dirs[0], file]) != output_dirs[0]) \
|
||||
@@ -709,6 +822,7 @@ class BatchManager:
|
||||
self.outputs = {}
|
||||
self.unique_id = None
|
||||
self.has_closed_inputs = False
|
||||
self.total_frames = float('inf')
|
||||
def reset(self):
|
||||
self.close_inputs()
|
||||
for key in self.outputs:
|
||||
@@ -733,7 +847,7 @@ class BatchManager:
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"frames_per_batch": ("INT", {"default": 16, "min": 1, "max": 128, "step": 1})
|
||||
"frames_per_batch": ("INT", {"default": 16, "min": 1, "max": BIGMAX, "step": 1})
|
||||
},
|
||||
"hidden": {
|
||||
"prompt": "PROMPT",
|
||||
@@ -755,6 +869,9 @@ class BatchManager:
|
||||
self.reset()
|
||||
self.frames_per_batch = frames_per_batch
|
||||
self.unique_id = unique_id
|
||||
else:
|
||||
num_batches = (self.total_frames+self.frames_per_batch-1)//frames_per_batch
|
||||
print(f'Meta-Batch {requeue}/{num_batches}')
|
||||
#onExecuted seems to not be called unless some message is sent
|
||||
return (self,)
|
||||
|
||||
@@ -787,7 +904,7 @@ class VideoInfo:
|
||||
|
||||
def get_video_info(self, video_info):
|
||||
keys = ["fps", "frame_count", "duration", "width", "height"]
|
||||
|
||||
|
||||
source_info = []
|
||||
loaded_info = []
|
||||
|
||||
@@ -821,7 +938,7 @@ class VideoInfoSource:
|
||||
|
||||
def get_video_info(self, video_info):
|
||||
keys = ["fps", "frame_count", "duration", "width", "height"]
|
||||
|
||||
|
||||
source_info = []
|
||||
|
||||
for key in keys:
|
||||
@@ -853,7 +970,7 @@ class VideoInfoLoaded:
|
||||
|
||||
def get_video_info(self, video_info):
|
||||
keys = ["fps", "frame_count", "duration", "width", "height"]
|
||||
|
||||
|
||||
loaded_info = []
|
||||
|
||||
for key in keys:
|
||||
@@ -861,22 +978,80 @@ class VideoInfoLoaded:
|
||||
|
||||
return (*loaded_info,)
|
||||
|
||||
class SelectFilename:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {"filenames": ("VHS_FILENAMES",), "index": ("INT", {"default": -1, "step": 1, "min": -1})}}
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES =("Filename",)
|
||||
CATEGORY = "Video Helper Suite 🎥🅥🅗🅢"
|
||||
FUNCTION = "select_filename"
|
||||
|
||||
def select_filename(self, filenames, index):
|
||||
return (filenames[1][index],)
|
||||
class Unbatch:
|
||||
class Any(str):
|
||||
def __ne__(self, other):
|
||||
return False
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {"batched": ("*",)}}
|
||||
RETURN_TYPES = (Any('*'),)
|
||||
INPUT_IS_LIST = True
|
||||
RETURN_NAMES =("unbatched",)
|
||||
CATEGORY = "Video Helper Suite 🎥🅥🅗🅢"
|
||||
FUNCTION = "unbatch"
|
||||
def unbatch(self, batched):
|
||||
if isinstance(batched[0], torch.Tensor):
|
||||
return (torch.cat(batched),)
|
||||
if isinstance(batched[0], dict):
|
||||
out = batched[0].copy()
|
||||
if 'samples' in out:
|
||||
out['samples'] = torch.cat([x['samples'] for x in batched])
|
||||
if 'waveform' in out:
|
||||
out['waveform'] = torch.cat([x['waveform'] for x in batched])
|
||||
out.pop('batch_index', None)
|
||||
return (out,)
|
||||
return (functools.reduce(lambda x,y: x+y, batched),)
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(cls, input_types):
|
||||
return True
|
||||
class SelectLatest:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {"filename_prefix": ("STRING", {'default': 'output/AnimateDiff', 'vhs_path_extensions': []}),
|
||||
"filename_postfix": ("STRING", {"placeholder": ".webm"})}}
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES =("Filename",)
|
||||
CATEGORY = "Video Helper Suite 🎥🅥🅗🅢"
|
||||
FUNCTION = "select_latest"
|
||||
EXPERIMENTAL = True
|
||||
|
||||
def select_latest(self, filename_prefix, filename_postfix):
|
||||
assert False, "Not Reachable"
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"VHS_VideoCombine": VideoCombine,
|
||||
"VHS_LoadVideo": LoadVideoUpload,
|
||||
"VHS_LoadVideoPath": LoadVideoPath,
|
||||
"VHS_LoadVideoFFmpeg": LoadVideoFFmpegUpload,
|
||||
"VHS_LoadVideoFFmpegPath": LoadVideoFFmpegPath,
|
||||
"VHS_LoadImagePath": LoadImagePath,
|
||||
"VHS_LoadImages": LoadImagesFromDirectoryUpload,
|
||||
"VHS_LoadImagesPath": LoadImagesFromDirectoryPath,
|
||||
"VHS_LoadAudio": LoadAudio,
|
||||
"VHS_LoadAudioUpload": LoadAudioUpload,
|
||||
"VHS_AudioToVHSAudio": AudioToVHSAudio,
|
||||
"VHS_VHSAudioToAudio": VHSAudioToAudio,
|
||||
"VHS_PruneOutputs": PruneOutputs,
|
||||
"VHS_BatchManager": BatchManager,
|
||||
"VHS_VideoInfo": VideoInfo,
|
||||
"VHS_VideoInfoSource": VideoInfoSource,
|
||||
"VHS_VideoInfoLoaded": VideoInfoLoaded,
|
||||
"VHS_SelectFilename": SelectFilename,
|
||||
# Batched Nodes
|
||||
"VHS_VAEEncodeBatched": VAEEncodeBatched,
|
||||
"VHS_VAEDecodeBatched": VAEDecodeBatched,
|
||||
# Latent and Image nodes
|
||||
"VHS_SplitLatents": SplitLatents,
|
||||
"VHS_SplitImages": SplitImages,
|
||||
@@ -884,50 +1059,62 @@ NODE_CLASS_MAPPINGS = {
|
||||
"VHS_MergeLatents": MergeLatents,
|
||||
"VHS_MergeImages": MergeImages,
|
||||
"VHS_MergeMasks": MergeMasks,
|
||||
"VHS_SelectEveryNthLatent": SelectEveryNthLatent,
|
||||
"VHS_SelectEveryNthImage": SelectEveryNthImage,
|
||||
"VHS_SelectEveryNthMask": SelectEveryNthMask,
|
||||
"VHS_GetLatentCount": GetLatentCount,
|
||||
"VHS_GetImageCount": GetImageCount,
|
||||
"VHS_GetMaskCount": GetMaskCount,
|
||||
"VHS_DuplicateLatents": DuplicateLatents,
|
||||
"VHS_DuplicateImages": DuplicateImages,
|
||||
"VHS_DuplicateMasks": DuplicateMasks,
|
||||
# Batched Nodes
|
||||
"VHS_VAEEncodeBatched": VAEEncodeBatched,
|
||||
"VHS_VAEDecodeBatched": VAEDecodeBatched,
|
||||
"VHS_DuplicateLatents": RepeatLatents,
|
||||
"VHS_DuplicateImages": RepeatImages,
|
||||
"VHS_DuplicateMasks": RepeatMasks,
|
||||
"VHS_SelectEveryNthLatent": SelectEveryNthLatent,
|
||||
"VHS_SelectEveryNthImage": SelectEveryNthImage,
|
||||
"VHS_SelectEveryNthMask": SelectEveryNthMask,
|
||||
"VHS_SelectLatents": SelectLatents,
|
||||
"VHS_SelectImages": SelectImages,
|
||||
"VHS_SelectMasks": SelectMasks,
|
||||
"VHS_Unbatch": Unbatch,
|
||||
"VHS_SelectLatest": SelectLatest,
|
||||
}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"VHS_VideoCombine": "Video Combine 🎥🅥🅗🅢",
|
||||
"VHS_LoadVideo": "Load Video (Upload) 🎥🅥🅗🅢",
|
||||
"VHS_LoadVideoPath": "Load Video (Path) 🎥🅥🅗🅢",
|
||||
"VHS_LoadVideoFFmpeg": "Load Video FFmpeg (Upload) 🎥🅥🅗🅢",
|
||||
"VHS_LoadVideoFFmpegPath": "Load Video FFmpeg (Path) 🎥🅥🅗🅢",
|
||||
"VHS_LoadImagePath": "Load Image (Path) 🎥🅥🅗🅢",
|
||||
"VHS_LoadImages": "Load Images (Upload) 🎥🅥🅗🅢",
|
||||
"VHS_LoadImagesPath": "Load Images (Path) 🎥🅥🅗🅢",
|
||||
"VHS_LoadAudio": "Load Audio (Path)🎥🅥🅗🅢",
|
||||
"VHS_LoadAudioUpload": "Load Audio (Upload)🎥🅥🅗🅢",
|
||||
"VHS_AudioToVHSAudio": "Audio to legacy VHS_AUDIO🎥🅥🅗🅢",
|
||||
"VHS_VHSAudioToAudio": "Legacy VHS_AUDIO to Audio🎥🅥🅗🅢",
|
||||
"VHS_PruneOutputs": "Prune Outputs 🎥🅥🅗🅢",
|
||||
"VHS_BatchManager": "Meta Batch Manager 🎥🅥🅗🅢",
|
||||
"VHS_VideoInfo": "Video Info 🎥🅥🅗🅢",
|
||||
"VHS_VideoInfoSource": "Video Info (Source) 🎥🅥🅗🅢",
|
||||
"VHS_VideoInfoLoaded": "Video Info (Loaded) 🎥🅥🅗🅢",
|
||||
# Latent and Image nodes
|
||||
"VHS_SplitLatents": "Split Latent Batch 🎥🅥🅗🅢",
|
||||
"VHS_SplitImages": "Split Image Batch 🎥🅥🅗🅢",
|
||||
"VHS_SplitMasks": "Split Mask Batch 🎥🅥🅗🅢",
|
||||
"VHS_MergeLatents": "Merge Latent Batches 🎥🅥🅗🅢",
|
||||
"VHS_MergeImages": "Merge Image Batches 🎥🅥🅗🅢",
|
||||
"VHS_MergeMasks": "Merge Mask Batches 🎥🅥🅗🅢",
|
||||
"VHS_SelectEveryNthLatent": "Select Every Nth Latent 🎥🅥🅗🅢",
|
||||
"VHS_SelectEveryNthImage": "Select Every Nth Image 🎥🅥🅗🅢",
|
||||
"VHS_SelectEveryNthMask": "Select Every Nth Mask 🎥🅥🅗🅢",
|
||||
"VHS_GetLatentCount": "Get Latent Count 🎥🅥🅗🅢",
|
||||
"VHS_GetImageCount": "Get Image Count 🎥🅥🅗🅢",
|
||||
"VHS_GetMaskCount": "Get Mask Count 🎥🅥🅗🅢",
|
||||
"VHS_DuplicateLatents": "Duplicate Latent Batch 🎥🅥🅗🅢",
|
||||
"VHS_DuplicateImages": "Duplicate Image Batch 🎥🅥🅗🅢",
|
||||
"VHS_DuplicateMasks": "Duplicate Mask Batch 🎥🅥🅗🅢",
|
||||
"VHS_SelectFilename": "Select Filename 🎥🅥🅗🅢",
|
||||
# Batched Nodes
|
||||
"VHS_VAEEncodeBatched": "VAE Encode Batched 🎥🅥🅗🅢",
|
||||
"VHS_VAEDecodeBatched": "VAE Decode Batched 🎥🅥🅗🅢",
|
||||
# Latent and Image nodes
|
||||
"VHS_SplitLatents": "Split Latents 🎥🅥🅗🅢",
|
||||
"VHS_SplitImages": "Split Images 🎥🅥🅗🅢",
|
||||
"VHS_SplitMasks": "Split Masks 🎥🅥🅗🅢",
|
||||
"VHS_MergeLatents": "Merge Latents 🎥🅥🅗🅢",
|
||||
"VHS_MergeImages": "Merge Images 🎥🅥🅗🅢",
|
||||
"VHS_MergeMasks": "Merge Masks 🎥🅥🅗🅢",
|
||||
"VHS_GetLatentCount": "Get Latent Count 🎥🅥🅗🅢",
|
||||
"VHS_GetImageCount": "Get Image Count 🎥🅥🅗🅢",
|
||||
"VHS_GetMaskCount": "Get Mask Count 🎥🅥🅗🅢",
|
||||
"VHS_DuplicateLatents": "Repeat Latents 🎥🅥🅗🅢",
|
||||
"VHS_DuplicateImages": "Repeat Images 🎥🅥🅗🅢",
|
||||
"VHS_DuplicateMasks": "Repeat Masks 🎥🅥🅗🅢",
|
||||
"VHS_SelectEveryNthLatent": "Select Every Nth Latent 🎥🅥🅗🅢",
|
||||
"VHS_SelectEveryNthImage": "Select Every Nth Image 🎥🅥🅗🅢",
|
||||
"VHS_SelectEveryNthMask": "Select Every Nth Mask 🎥🅥🅗🅢",
|
||||
"VHS_SelectLatents": "Select Latents 🎥🅥🅗🅢",
|
||||
"VHS_SelectImages": "Select Images 🎥🅥🅗🅢",
|
||||
"VHS_SelectMasks": "Select Masks 🎥🅥🅗🅢",
|
||||
"VHS_Unbatch": "Unbatch 🎥🅥🅗🅢",
|
||||
"VHS_SelectLatest": "Select Latest 🎥🅥🅗🅢",
|
||||
}
|
||||
|
||||
+216
-76
@@ -1,64 +1,42 @@
|
||||
import server
|
||||
import folder_paths
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
from .utils import is_url, get_sorted_dir_files_from_directory, ffmpeg_path, validate_sequence, is_safe_path, strip_path
|
||||
import re
|
||||
|
||||
import asyncio
|
||||
import av
|
||||
|
||||
from .utils import is_url, get_sorted_dir_files_from_directory, ffmpeg_path, \
|
||||
validate_sequence, is_safe_path, strip_path, try_download_video, ENCODE_ARGS
|
||||
from comfy.k_diffusion.utils import FolderOfImages
|
||||
|
||||
|
||||
web = server.web
|
||||
|
||||
@server.PromptServer.instance.routes.get("/vhs/viewvideo")
|
||||
@server.PromptServer.instance.routes.get("/viewvideo")
|
||||
async def view_video(request):
|
||||
query = request.rel_url.query
|
||||
if "filename" not in query:
|
||||
return web.Response(status=404)
|
||||
filename = query["filename"]
|
||||
path_res = await resolve_path(query)
|
||||
if isinstance(path_res, web.Response):
|
||||
return path_res
|
||||
file, filename, output_dir = path_res
|
||||
|
||||
#Path code misformats urls on windows and must be skipped
|
||||
if is_url(filename):
|
||||
file = filename
|
||||
else:
|
||||
filename, output_dir = folder_paths.annotated_filepath(filename)
|
||||
|
||||
type = request.rel_url.query.get("type", "output")
|
||||
if type == "path":
|
||||
#special case for path_based nodes
|
||||
#NOTE: output_dir may be empty, but non-None
|
||||
output_dir, filename = os.path.split(strip_path(filename))
|
||||
if output_dir is None:
|
||||
output_dir = folder_paths.get_directory_by_type(type)
|
||||
|
||||
if output_dir is None:
|
||||
return web.Response(status=400)
|
||||
|
||||
if not is_safe_path(output_dir):
|
||||
return web.Response(status=403)
|
||||
|
||||
if "subfolder" in request.rel_url.query:
|
||||
output_dir = os.path.join(output_dir, request.rel_url.query["subfolder"])
|
||||
|
||||
filename = os.path.basename(filename)
|
||||
file = os.path.join(output_dir, filename)
|
||||
|
||||
if query.get('format', 'video') == 'folder':
|
||||
if not os.path.isdir(file):
|
||||
return web.Response(status=404)
|
||||
else:
|
||||
if not os.path.isfile(file) and not validate_sequence(file):
|
||||
return web.Response(status=404)
|
||||
if ffmpeg_path is None:
|
||||
#Don't just return file, that provides arbitrary read access to any file
|
||||
if is_safe_path(output_dir, strict=True):
|
||||
return web.FileResponse(path=file)
|
||||
|
||||
frame_rate = query.get('frame_rate', 8)
|
||||
if query.get('format', 'video') == "folder":
|
||||
#Check that folder contains some valid image file, get it's extension
|
||||
#ffmpeg seems to not support list globs, so support for mixed extensions seems unfeasible
|
||||
os.makedirs(folder_paths.get_temp_directory(), exist_ok=True)
|
||||
concat_file = os.path.join(folder_paths.get_temp_directory(), "image_sequence_preview.txt")
|
||||
skip_first_images = int(query.get('skip_first_images', 0))
|
||||
select_every_nth = int(query.get('select_every_nth', 1))
|
||||
select_every_nth = int(query.get('select_every_nth', 1)) or 1
|
||||
valid_images = get_sorted_dir_files_from_directory(file, skip_first_images, select_every_nth, FolderOfImages.IMG_EXTENSIONS)
|
||||
if len(valid_images) == 0:
|
||||
return web.Response(status=400)
|
||||
return web.Response(status=204)
|
||||
with open(concat_file, "w") as f:
|
||||
f.write("ffconcat version 1.0\n")
|
||||
for path in valid_images:
|
||||
@@ -69,15 +47,49 @@ async def view_video(request):
|
||||
in_args = ["-i", file]
|
||||
if '%' in file:
|
||||
in_args = ['-framerate', str(frame_rate)] + in_args
|
||||
#Do prepass to pull info
|
||||
#breaks skip_first frames if this default is ever actually needed
|
||||
base_fps = 30
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(ffmpeg_path, *in_args, '-t',
|
||||
'0','-f', 'null','-', stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, stdin=subprocess.DEVNULL)
|
||||
_, res_stderr = await proc.communicate()
|
||||
|
||||
args = [ffmpeg_path, "-v", "error"] + in_args
|
||||
match = re.search(': Video: (\\w+) .+, (\\d+) fps,', res_stderr.decode(*ENCODE_ARGS))
|
||||
if match:
|
||||
base_fps = float(match.group(2))
|
||||
if match.group(1) == 'vp9':
|
||||
#force libvpx for transparency
|
||||
in_args = ['-c:v', 'libvpx-vp9'] + in_args
|
||||
except subprocess.CalledProcessError as e:
|
||||
print("An error occurred in the ffmpeg prepass:\n" \
|
||||
+ e.stderr.decode(*ENCODE_ARGS))
|
||||
return web.Response(status=500)
|
||||
vfilters = []
|
||||
if int(query.get('force_rate',0)) != 0:
|
||||
vfilters.append("fps=fps="+query['force_rate'] + ":round=up:start_time=0.001")
|
||||
if int(query.get('skip_first_frames', 0)) > 0:
|
||||
vfilters.append(f"select=gt(n\\,{int(query['skip_first_frames'])-1})")
|
||||
if int(query.get('select_every_nth', 1)) > 1:
|
||||
vfilters.append(f"select=not(mod(n\\,{query['select_every_nth']}))")
|
||||
target_rate = float(query.get('force_rate', 0)) or base_fps
|
||||
modified_rate = target_rate / (float(query.get('select_every_nth',1)) or 1)
|
||||
start_time = 0
|
||||
if 'start_time' in query:
|
||||
start_time = float(query['start_time'])
|
||||
elif float(query.get('skip_first_frames', 0)) > 0:
|
||||
start_time = float(query.get('skip_first_frames'))/target_rate
|
||||
if start_time > 1/modified_rate:
|
||||
start_time += 1/modified_rate
|
||||
if start_time > 0:
|
||||
if start_time > 4:
|
||||
post_seek = ['-ss', '4']
|
||||
pre_seek = ['-ss', str(start_time - 4)]
|
||||
else:
|
||||
post_seek = ['-ss', str(start_time)]
|
||||
pre_seek = []
|
||||
else:
|
||||
pre_seek = []
|
||||
post_seek = []
|
||||
|
||||
args = [ffmpeg_path, "-v", "error"] + pre_seek + in_args + post_seek
|
||||
if target_rate != 0:
|
||||
args += ['-r', str(modified_rate)]
|
||||
if query.get('force_size','Disabled') != "Disabled":
|
||||
size = query['force_size'].split('x')
|
||||
if size[0] == '?' or size[1] == '?':
|
||||
@@ -90,47 +102,175 @@ async def view_video(request):
|
||||
vfilters.append(f"crop=if(gt({ar}\\,a)\\,iw\\,ih*{ar}):if(gt({ar}\\,a)\\,iw/{ar}\\,ih)")
|
||||
size = ':'.join(size)
|
||||
vfilters.append(f"scale={size}")
|
||||
vfilters.append("setpts=PTS-STARTPTS")
|
||||
if len(vfilters) > 0:
|
||||
args += ["-vf", ",".join(vfilters)]
|
||||
if int(query.get('frame_load_cap', 0)) > 0:
|
||||
args += ["-frames:v", query['frame_load_cap']]
|
||||
if float(query.get('frame_load_cap', 0)) > 0:
|
||||
args += ["-frames:v", query['frame_load_cap'].split('.')[0]]
|
||||
#TODO:reconsider adding high frame cap/setting default frame cap on node
|
||||
if query.get('deadline', 'realtime') == 'good':
|
||||
deadline = 'good'
|
||||
else:
|
||||
deadline = 'realtime'
|
||||
|
||||
args += ['-c:v', 'libvpx-vp9','-deadline', 'realtime', '-cpu-used', '8', '-f', 'webm', '-']
|
||||
args += ['-c:v', 'libvpx-vp9','-deadline', deadline, '-cpu-used', '8', '-f', 'webm', '-']
|
||||
|
||||
try:
|
||||
with subprocess.Popen(args, stdout=subprocess.PIPE) as proc:
|
||||
try:
|
||||
resp = web.StreamResponse()
|
||||
resp.content_type = 'video/webm'
|
||||
resp.headers["Content-Disposition"] = f"filename=\"{filename}\""
|
||||
await resp.prepare(request)
|
||||
while True:
|
||||
bytes_read = proc.stdout.read()
|
||||
if bytes_read is None:
|
||||
#TODO: check for timeout here
|
||||
time.sleep(.1)
|
||||
continue
|
||||
if len(bytes_read) == 0:
|
||||
break
|
||||
await resp.write(bytes_read)
|
||||
except ConnectionResetError as e:
|
||||
#Kill ffmpeg before stdout closes
|
||||
proc.kill()
|
||||
except ConnectionError as e:
|
||||
#Kill ffmpeg before stdout closes
|
||||
proc.kill()
|
||||
proc = await asyncio.create_subprocess_exec(*args, stdout=subprocess.PIPE,
|
||||
stdin=subprocess.DEVNULL)
|
||||
try:
|
||||
resp = web.StreamResponse()
|
||||
resp.content_type = 'video/webm'
|
||||
resp.headers["Content-Disposition"] = f"filename=\"{filename}\""
|
||||
await resp.prepare(request)
|
||||
while len(bytes_read := await proc.stdout.read(2**20)) != 0:
|
||||
await resp.write(bytes_read)
|
||||
#Of dubious value given frequency of kill calls, but more correct
|
||||
await proc.wait()
|
||||
except (ConnectionResetError, ConnectionError) as e:
|
||||
proc.kill()
|
||||
except BrokenPipeError as e:
|
||||
pass
|
||||
return resp
|
||||
@server.PromptServer.instance.routes.get("/vhs/viewaudio")
|
||||
async def view_audio(request):
|
||||
query = request.rel_url.query
|
||||
path_res = await resolve_path(query)
|
||||
if isinstance(path_res, web.Response):
|
||||
return path_res
|
||||
file, filename, output_dir = path_res
|
||||
if ffmpeg_path is None:
|
||||
#Don't just return file, that provides arbitrary read access to any file
|
||||
if is_safe_path(output_dir, strict=True):
|
||||
return web.FileResponse(path=file)
|
||||
|
||||
in_args = ["-i", file]
|
||||
start_time = 0
|
||||
if 'start_time' in query:
|
||||
start_time = float(query['start_time'])
|
||||
args = [ffmpeg_path, "-v", "error", '-vn'] + in_args + ['-ss', str(start_time)]
|
||||
if float(query.get('duration', 0)) > 0:
|
||||
args += ['-t', str(query['duration'])]
|
||||
if query.get('deadline', 'realtime') == 'good':
|
||||
deadline = 'good'
|
||||
else:
|
||||
deadline = 'realtime'
|
||||
|
||||
args += ['-c:a', 'libopus','-deadline', deadline, '-cpu-used', '8', '-f', 'webm', '-']
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(*args, stdout=subprocess.PIPE,
|
||||
stdin=subprocess.DEVNULL)
|
||||
try:
|
||||
resp = web.StreamResponse()
|
||||
resp.content_type = 'audio/webm'
|
||||
resp.headers["Content-Disposition"] = f"filename=\"{filename}\""
|
||||
await resp.prepare(request)
|
||||
while len(bytes_read := await proc.stdout.read(2**20)) != 0:
|
||||
await resp.write(bytes_read)
|
||||
#Of dubious value given frequency of kill calls, but more correct
|
||||
await proc.wait()
|
||||
except (ConnectionResetError, ConnectionError) as e:
|
||||
proc.kill()
|
||||
except BrokenPipeError as e:
|
||||
pass
|
||||
return resp
|
||||
|
||||
query_cache = {}
|
||||
@server.PromptServer.instance.routes.get("/vhs/queryvideo")
|
||||
async def query_video(request):
|
||||
query = request.rel_url.query
|
||||
filepath = await resolve_path(query)
|
||||
#TODO: cache lookup
|
||||
if isinstance(filepath, web.Response):
|
||||
return filepath
|
||||
filepath = filepath[0]
|
||||
if filepath.endswith(".webp"):
|
||||
# ffmpeg doesn't support decoding animated WebP https://trac.ffmpeg.org/ticket/4907
|
||||
return web.json_response({})
|
||||
if filepath in query_cache and query_cache[filepath][0] == os.stat(filepath).st_mtime:
|
||||
source = query_cache[filepath][1]
|
||||
else:
|
||||
source = {}
|
||||
try:
|
||||
with av.open(filepath) as cont:
|
||||
stream = cont.streams.video[0]
|
||||
source['fps'] = float(stream.average_rate)
|
||||
source['duration'] = float(cont.duration / av.time_base)
|
||||
|
||||
if stream.codec_context.name == 'vp9':
|
||||
cc = av.Codec('libvpx-vp9', 'r').create()
|
||||
else:
|
||||
cc = stream
|
||||
def fit():
|
||||
for packet in cont.demux(video=0):
|
||||
yield from cc.decode(packet)
|
||||
frame = next(fit())
|
||||
|
||||
source['size'] = [frame.width, frame.height]
|
||||
source['alpha'] = 'a' in frame.format.name
|
||||
source['frames'] = stream.metadata.get('NUMBER_OF_FRAMES', round(source['duration'] * source['fps']))
|
||||
query_cache[filepath] = (os.stat(filepath).st_mtime, source)
|
||||
except Exception:
|
||||
pass
|
||||
if not 'frames' in source:
|
||||
return web.json_response({})
|
||||
loaded = {}
|
||||
loaded['duration'] = source['duration']
|
||||
loaded['duration'] -= float(query.get('start_time',0))
|
||||
loaded['fps'] = float(query.get('force_rate', 0)) or source.get('fps',1)
|
||||
loaded['duration'] -= int(query.get('skip_first_frames', 0)) / loaded['fps']
|
||||
loaded['fps'] /= int(query.get('select_every_nth', 1)) or 1
|
||||
loaded['frames'] = round(loaded['duration'] * loaded['fps'])
|
||||
return web.json_response({'source': source, 'loaded': loaded})
|
||||
|
||||
async def resolve_path(query):
|
||||
if "filename" not in query:
|
||||
return web.Response(status=204)
|
||||
filename = query["filename"]
|
||||
|
||||
#Path code misformats urls on windows and must be skipped
|
||||
if is_url(filename):
|
||||
file = await asyncio.to_thread(try_download_video, filename) or file
|
||||
filname, output_dir = os.path.split(file)
|
||||
return file, filename, output_dir
|
||||
else:
|
||||
filename, output_dir = folder_paths.annotated_filepath(filename)
|
||||
|
||||
type = query.get("type", "output")
|
||||
if type == "path":
|
||||
#special case for path_based nodes
|
||||
#NOTE: output_dir may be empty, but non-None
|
||||
output_dir, filename = os.path.split(strip_path(filename))
|
||||
if output_dir is None:
|
||||
output_dir = folder_paths.get_directory_by_type(type)
|
||||
|
||||
if output_dir is None:
|
||||
return web.Response(status=204)
|
||||
|
||||
if not is_safe_path(output_dir):
|
||||
return web.Response(status=204)
|
||||
|
||||
if "subfolder" in query:
|
||||
output_dir = os.path.join(output_dir, query["subfolder"])
|
||||
|
||||
filename = os.path.basename(filename)
|
||||
file = os.path.join(output_dir, filename)
|
||||
|
||||
if not os.path.exists(file):
|
||||
return web.Response(status=204)
|
||||
if query.get('format', 'video') == 'folder':
|
||||
if not os.path.isdir(file):
|
||||
return web.Response(status=204)
|
||||
else:
|
||||
if not os.path.isfile(file) and not validate_sequence(file):
|
||||
return web.Response(status=204)
|
||||
return file, filename, output_dir
|
||||
|
||||
@server.PromptServer.instance.routes.get("/vhs/getpath")
|
||||
@server.PromptServer.instance.routes.get("/getpath")
|
||||
async def get_path(request):
|
||||
query = request.rel_url.query
|
||||
if "path" not in query:
|
||||
return web.Response(status=404)
|
||||
return web.Response(status=204)
|
||||
#NOTE: path always ends in `/`, so this is functionally an lstrip
|
||||
path = os.path.abspath(strip_path(query["path"]))
|
||||
|
||||
@@ -145,10 +285,10 @@ async def get_path(request):
|
||||
if item.is_dir():
|
||||
valid_items.append(item.name + "/")
|
||||
continue
|
||||
if valid_extensions is None or item.name.split(".")[-1] in valid_extensions:
|
||||
if valid_extensions is None or item.name.split(".")[-1].lower() in valid_extensions:
|
||||
valid_items.append(item.name)
|
||||
except OSError:
|
||||
#Broken symlinks can throw a very unhelpful "Invalid argument"
|
||||
pass
|
||||
|
||||
valid_items.sort(key=lambda f: os.stat(os.path.join(path,f)).st_mtime)
|
||||
return web.json_response(valid_items)
|
||||
|
||||
+198
-11
@@ -4,21 +4,28 @@ from typing import Iterable
|
||||
import shutil
|
||||
import subprocess
|
||||
import re
|
||||
import time
|
||||
from collections.abc import Mapping
|
||||
from typing import Union
|
||||
import functools
|
||||
import torch
|
||||
from torch import Tensor
|
||||
|
||||
import server
|
||||
from .logger import logger
|
||||
import folder_paths
|
||||
|
||||
BIGMIN = -(2**53-1)
|
||||
BIGMAX = (2**53-1)
|
||||
|
||||
DIMMAX = 8192
|
||||
|
||||
ENCODE_ARGS = ("utf-8", 'backslashreplace')
|
||||
|
||||
def ffmpeg_suitability(path):
|
||||
try:
|
||||
version = subprocess.run([path, "-version"], check=True,
|
||||
capture_output=True).stdout.decode("utf-8")
|
||||
capture_output=True).stdout.decode(*ENCODE_ARGS)
|
||||
except:
|
||||
return 0
|
||||
score = 0
|
||||
@@ -36,6 +43,23 @@ def ffmpeg_suitability(path):
|
||||
score += int(copyright_year)
|
||||
return score
|
||||
|
||||
class MultiInput(str):
|
||||
def __new__(cls, string, allowed_types="*"):
|
||||
res = super().__new__(cls, string)
|
||||
res.allowed_types=allowed_types
|
||||
return res
|
||||
def __ne__(self, other):
|
||||
if self.allowed_types == "*" or other == "*":
|
||||
return False
|
||||
return other not in self.allowed_types
|
||||
imageOrLatent = MultiInput("IMAGE", ["IMAGE", "LATENT"])
|
||||
floatOrInt = MultiInput("FLOAT", ["FLOAT", "INT"])
|
||||
|
||||
class ContainsAll(dict):
|
||||
def __contains__(self, other):
|
||||
return True
|
||||
def __getitem__(self, key):
|
||||
return super().get(key, (None, {}))
|
||||
|
||||
if "VHS_FORCE_FFMPEG_PATH" in os.environ:
|
||||
ffmpeg_path = os.environ.get("VHS_FORCE_FFMPEG_PATH")
|
||||
@@ -73,9 +97,34 @@ if gifski_path is None:
|
||||
gifski_path = os.environ.get("JOV_GIFSKI", None)
|
||||
if gifski_path is None:
|
||||
gifski_path = shutil.which("gifski")
|
||||
ytdl_path = os.environ.get("VHS_YTDL", None) or shutil.which('yt-dlp') \
|
||||
or shutil.which('youtube-dl')
|
||||
download_history = {}
|
||||
def try_download_video(url):
|
||||
if ytdl_path is None:
|
||||
return None
|
||||
if url in download_history:
|
||||
return download_history[url]
|
||||
os.makedirs(folder_paths.get_temp_directory(), exist_ok=True)
|
||||
#Format information could be added to only download audio for Load Audio,
|
||||
#but this gets hairy if same url is also used for video.
|
||||
#Best to just always keep defaults
|
||||
#dl_format = ['-f', 'ba'] if is_audio else []
|
||||
try:
|
||||
res = subprocess.run([ytdl_path, "--print", "after_move:filepath",
|
||||
"-P", folder_paths.get_temp_directory(), url],
|
||||
capture_output=True, check=True)
|
||||
#strip newline
|
||||
file = res.stdout.decode(*ENCODE_ARGS)[:-1]
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise Exception("An error occurred in the yt-dl process:\n" \
|
||||
+ e.stderr.decode(*ENCODE_ARGS))
|
||||
file = None
|
||||
download_history[url] = file
|
||||
return file
|
||||
|
||||
def is_safe_path(path):
|
||||
if "VHS_STRICT_PATHS" not in os.environ:
|
||||
def is_safe_path(path, strict=False):
|
||||
if "VHS_STRICT_PATHS" not in os.environ and not strict:
|
||||
return True
|
||||
basedir = os.path.abspath('.')
|
||||
try:
|
||||
@@ -119,8 +168,14 @@ prompt_queue = server.PromptServer.instance.prompt_queue
|
||||
def requeue_workflow_unchecked():
|
||||
"""Requeues the current workflow without checking for multiple requeues"""
|
||||
currently_running = prompt_queue.currently_running
|
||||
(_, _, prompt, extra_data, outputs_to_execute) = next(iter(currently_running.values()))
|
||||
|
||||
value = next(iter(currently_running.values()))
|
||||
|
||||
# Handle both old (5 values) and new (6 values) ComfyUI versions
|
||||
if len(value) == 6:
|
||||
(_, prompt_id, prompt, extra_data, outputs_to_execute, _) = value
|
||||
else:
|
||||
(_, prompt_id, prompt, extra_data, outputs_to_execute) = value
|
||||
|
||||
#Ensure batch_managers are marked stale
|
||||
prompt = prompt.copy()
|
||||
for uid in prompt:
|
||||
@@ -132,13 +187,24 @@ def requeue_workflow_unchecked():
|
||||
number = -server.PromptServer.instance.number
|
||||
server.PromptServer.instance.number += 1
|
||||
prompt_id = str(server.uuid.uuid4())
|
||||
prompt_queue.put((number, prompt_id, prompt, extra_data, outputs_to_execute))
|
||||
# Put back with 6 elements to match what ComfyUI expects
|
||||
sensitive = value[5] if len(value) > 5 else {}
|
||||
prompt_queue.put((number, prompt_id, prompt, extra_data, outputs_to_execute, sensitive))
|
||||
|
||||
requeue_guard = [None, 0, 0, {}]
|
||||
|
||||
def requeue_workflow(requeue_required=(-1,True)):
|
||||
assert(len(prompt_queue.currently_running) == 1)
|
||||
global requeue_guard
|
||||
(run_number, _, prompt, _, _) = next(iter(prompt_queue.currently_running.values()))
|
||||
|
||||
value = next(iter(prompt_queue.currently_running.values()))
|
||||
|
||||
# Handle both old (5 values) and new (6 values) ComfyUI versions
|
||||
if len(value) == 6:
|
||||
(run_number, _, prompt, extra_data, outputs_to_execute, _) = value
|
||||
else:
|
||||
(run_number, _, prompt, extra_data, outputs_to_execute) = value
|
||||
|
||||
if requeue_guard[0] != run_number:
|
||||
#Calculate a count of how many outputs are managed by a batch manager
|
||||
managed_outputs=0
|
||||
@@ -166,10 +232,10 @@ def get_audio(file, start_time=0, duration=0):
|
||||
res = subprocess.run(args + ["-f", "f32le", "-"],
|
||||
capture_output=True, check=True)
|
||||
audio = torch.frombuffer(bytearray(res.stdout), dtype=torch.float32)
|
||||
match = re.search(', (\\d+) Hz, (\\w+), ',res.stderr.decode(*ENCODE_ARGS))
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.warning(f"Failed to extract audio from: {file}")
|
||||
audio = torch.zeros(1,2)
|
||||
match = re.search(', (\\d+) Hz, (\\w+), ',res.stderr.decode('utf-8'))
|
||||
raise Exception(f"VHS failed to extract audio from {file}:\n" \
|
||||
+ e.stderr.decode(*ENCODE_ARGS))
|
||||
if match:
|
||||
ar = int(match.group(1))
|
||||
#NOTE: Just throwing an error for other channel types right now
|
||||
@@ -199,7 +265,7 @@ class LazyAudioMap(Mapping):
|
||||
if self._dict is None:
|
||||
self._dict = get_audio(self.file, self.start_time, self.duration)
|
||||
return len(self._dict)
|
||||
def lazy_get_audio(file, start_time=0, duration=0):
|
||||
def lazy_get_audio(file, start_time=0, duration=0, **kwargs):
|
||||
return LazyAudioMap(file, start_time, duration)
|
||||
|
||||
def is_url(url):
|
||||
@@ -240,6 +306,8 @@ def hash_path(path):
|
||||
return "input"
|
||||
if is_url(path):
|
||||
return "url"
|
||||
if not os.path.isfile(path):
|
||||
return "DNE"
|
||||
return calculate_file_hash(strip_path(path))
|
||||
|
||||
|
||||
@@ -254,3 +322,122 @@ def validate_path(path, allow_none=False, allow_url=True):
|
||||
if not os.path.isfile(strip_path(path)):
|
||||
return "Invalid file path: {}".format(path)
|
||||
return is_safe_path(path)
|
||||
|
||||
|
||||
def validate_index(index: int, length: int=0, is_range: bool=False, allow_negative=False, allow_missing=False) -> int:
|
||||
# if part of range, do nothing
|
||||
if is_range:
|
||||
return index
|
||||
# otherwise, validate index
|
||||
# validate not out of range - only when latent_count is passed in
|
||||
if length > 0 and index > length-1 and not allow_missing:
|
||||
raise IndexError(f"Index '{index}' out of range for {length} item(s).")
|
||||
# if negative, validate not out of range
|
||||
if index < 0:
|
||||
if not allow_negative:
|
||||
raise IndexError(f"Negative indeces not allowed, but was '{index}'.")
|
||||
conv_index = length+index
|
||||
if conv_index < 0 and not allow_missing:
|
||||
raise IndexError(f"Index '{index}', converted to '{conv_index}' out of range for {length} item(s).")
|
||||
index = conv_index
|
||||
return index
|
||||
|
||||
|
||||
def convert_to_index_int(raw_index: str, length: int=0, is_range: bool=False, allow_negative=False, allow_missing=False) -> int:
|
||||
try:
|
||||
return validate_index(int(raw_index), length=length, is_range=is_range, allow_negative=allow_negative, allow_missing=allow_missing)
|
||||
except ValueError as e:
|
||||
raise ValueError(f"Index '{raw_index}' must be an integer.", e)
|
||||
|
||||
|
||||
def convert_str_to_indexes(indexes_str: str, length: int=0, allow_missing=False) -> list[int]:
|
||||
if not indexes_str:
|
||||
return []
|
||||
int_indexes = list(range(0, length))
|
||||
allow_negative = length > 0
|
||||
chosen_indexes = []
|
||||
# parse string - allow positive ints, negative ints, and ranges separated by ':'
|
||||
groups = indexes_str.split(",")
|
||||
groups = [g.strip() for g in groups]
|
||||
for g in groups:
|
||||
# parse range of indeces (e.g. 2:16)
|
||||
if ':' in g:
|
||||
index_range = g.split(":", 2)
|
||||
index_range = [r.strip() for r in index_range]
|
||||
|
||||
start_index = index_range[0]
|
||||
if len(start_index) > 0:
|
||||
start_index = convert_to_index_int(start_index, length=length, is_range=True, allow_negative=allow_negative, allow_missing=allow_missing)
|
||||
else:
|
||||
start_index = 0
|
||||
end_index = index_range[1]
|
||||
if len(end_index) > 0:
|
||||
end_index = convert_to_index_int(end_index, length=length, is_range=True, allow_negative=allow_negative, allow_missing=allow_missing)
|
||||
else:
|
||||
end_index = length
|
||||
# support step as well, to allow things like reversing, every-other, etc.
|
||||
step = 1
|
||||
if len(index_range) > 2:
|
||||
step = index_range[2]
|
||||
if len(step) > 0:
|
||||
step = convert_to_index_int(step, length=length, is_range=True, allow_negative=True, allow_missing=True)
|
||||
else:
|
||||
step = 1
|
||||
# if latents were passed in, base indeces on known latent count
|
||||
if len(int_indexes) > 0:
|
||||
chosen_indexes.extend(int_indexes[start_index:end_index][::step])
|
||||
# otherwise, assume indeces are valid
|
||||
else:
|
||||
chosen_indexes.extend(list(range(start_index, end_index, step)))
|
||||
# parse individual indeces
|
||||
else:
|
||||
chosen_indexes.append(convert_to_index_int(g, length=length, allow_negative=allow_negative, allow_missing=allow_missing))
|
||||
return chosen_indexes
|
||||
|
||||
|
||||
def select_indexes(input_obj: Union[Tensor, list], idxs: list):
|
||||
if type(input_obj) == Tensor:
|
||||
return input_obj[idxs]
|
||||
else:
|
||||
return [input_obj[i] for i in idxs]
|
||||
|
||||
def merge_filter_args(args, ftype="-vf"):
|
||||
#TODO This doesn't account for filter_complex
|
||||
#Will likely need to convert all filters to filter complex in the future
|
||||
#But that requires source/output deduplication
|
||||
try:
|
||||
start_index = args.index(ftype)+1
|
||||
index = start_index
|
||||
while True:
|
||||
index = args.index(ftype, index)
|
||||
args[start_index] += ',' + args[index+1]
|
||||
args.pop(index)
|
||||
args.pop(index)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def select_indexes_from_str(input_obj: Union[Tensor, list], indexes: str, err_if_missing=True, err_if_empty=True):
|
||||
real_idxs = convert_str_to_indexes(indexes, len(input_obj), allow_missing=not err_if_missing)
|
||||
if err_if_empty and len(real_idxs) == 0:
|
||||
raise Exception(f"Nothing was selected based on indexes found in '{indexes}'.")
|
||||
return select_indexes(input_obj, real_idxs)
|
||||
|
||||
def hook(obj, attr):
|
||||
def dec(f):
|
||||
f = functools.update_wrapper(f, getattr(obj,attr))
|
||||
setattr(obj,attr,f)
|
||||
return f
|
||||
return dec
|
||||
|
||||
def cached(duration):
|
||||
def dec(f):
|
||||
cached_ret = None
|
||||
cache_time = 0
|
||||
def cached_func():
|
||||
nonlocal cache_time, cached_ret
|
||||
if time.time() > cache_time + duration or cached_ret is None:
|
||||
cache_time = time.time()
|
||||
cached_ret = f()
|
||||
return cached_ret
|
||||
return cached_func
|
||||
return dec
|
||||
|
||||
+1715
-396
File diff suppressed because it is too large
Load Diff
+15
-16
@@ -11,7 +11,7 @@ function getVideoMetadata(file) {
|
||||
let decoder = new TextDecoder();
|
||||
// Check for known valid magic strings
|
||||
if (dataView.getUint32(0) == 0x1A45DFA3) {
|
||||
//webm
|
||||
//webm/mkv (both use EBML/Matroska format)
|
||||
//see http://wiki.webmproject.org/webm-metadata/global-metadata
|
||||
//and https://www.matroska.org/technical/elements.html
|
||||
//contrary to specs, tag seems consistently at start
|
||||
@@ -31,7 +31,7 @@ function getVideoMetadata(file) {
|
||||
if (n_octets < 4) {//250MB sanity cutoff
|
||||
let length = (vint >> (8*(4-n_octets))) & ~(1 << (7*n_octets));
|
||||
const content = decoder.decode(videoData.slice(offset+2+n_octets, offset+2+n_octets+length));
|
||||
const json = JSON.parse(content);
|
||||
let json = JSON.parse(content);
|
||||
r(json);
|
||||
return;
|
||||
}
|
||||
@@ -61,10 +61,9 @@ function getVideoMetadata(file) {
|
||||
}
|
||||
} else {
|
||||
console.error("Unknown magic: " + dataView.getUint32(0))
|
||||
r();
|
||||
return;
|
||||
}
|
||||
|
||||
r();
|
||||
return;
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
@@ -77,26 +76,26 @@ function isVideoFile(file) {
|
||||
if (file?.name?.endsWith(".mp4")) {
|
||||
return true;
|
||||
}
|
||||
if (file?.name?.endsWith(".mkv")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
let originalHandleFile = app.handleFile;
|
||||
app.handleFile = handleFile;
|
||||
let fileInput = document.getElementById("comfy-file-input")
|
||||
//hijack comfy-file-input to allow webm/mp4/mkv
|
||||
fileInput.accept += ",video/webm,video/mp4,video/x-matroska";
|
||||
|
||||
async function handleFile(file) {
|
||||
if (file?.type?.startsWith("video/") || isVideoFile(file)) {
|
||||
const videoInfo = await getVideoMetadata(file);
|
||||
if (videoInfo) {
|
||||
if (videoInfo.workflow) {
|
||||
|
||||
app.loadGraphData(videoInfo.workflow);
|
||||
}
|
||||
//Potentially check for/parse A1111 metadata here.
|
||||
if (videoInfo?.workflow) {
|
||||
await app.loadGraphData(videoInfo.workflow);
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return await originalHandleFile.apply(this, arguments);
|
||||
}
|
||||
return await originalHandleFile.apply(this, arguments);
|
||||
}
|
||||
|
||||
//hijack comfy-file-input to allow webm/mp4
|
||||
document.getElementById("comfy-file-input").accept += ",video/webm,video/mp4";
|
||||
|
||||
Reference in New Issue
Block a user