first commit

This commit is contained in:
yolain
2025-10-06 14:22:54 +08:00
commit a49e824bd6
5 changed files with 1183 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
__pycache__
.DS_Store
*.cache
*.ini
*.bak
.vscode/
.idea/
uni/
custom_presets/**
workflows/**
node.tar.gz
py/
front/
prompts/
web/dev/**
web/stable/**
web/wass/**
dist/
.pyarmor/
config.yaml
*.bat
*.log
*.bak
build_cloud.sh
+38
View File
@@ -0,0 +1,38 @@
Enhanced the comfyui savevideo node to support previewing and saving videos containing alpha channels.
<img width="100%" height="auto" alt="Image" src="https://github-production-user-asset-6210df.s3.amazonaws.com/73304135/497659951-46496759-daf4-4749-9644-b7e795560a69.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20251006%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20251006T061801Z&X-Amz-Expires=300&X-Amz-Signature=69ccad19ceb69edb1d89359b0cdf9c5717c546a4abd3c1f1b90e40f61259686c&X-Amz-SignedHeaders=host" />
### 🔨Wan-Alpha Video Usage
1. Download models
- The Wan DiT base model: [wan2.1_t2v_14B_fp16.safetensors](https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/blob/main/split_files/diffusion_models/wan2.1_t2v_14B_fp16.safetensors)
- The Wan text encoder: [umt5_xxl_fp8_e4m3fn_scaled.safetensors](https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/blob/main/split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors)
- The LightX2V model: [lightx2v_T2V_14B_cfg_step_distill_v2_lora_rank64_bf16.safetensors](https://huggingface.co/Kijai/WanVideo_comfy/blob/main/Lightx2v/lightx2v_T2V_14B_cfg_step_distill_v2_lora_rank64_bf16.safetensors)
- RGBA Dora: [epoch-13-1500_changed.safetensors](https://huggingface.co/htdong/Wan-Alpha_ComfyUI/blob/main/epoch-13-1500_changed.safetensors)
- RGB VAE Decoder: [wan_alpha_2.1_vae_rgb_channel.safetensors.safetensors](https://huggingface.co/htdong/Wan-Alpha_ComfyUI/blob/main/wan_alpha_2.1_vae_rgb_channel.safetensors.safetensors)
- Alpha VAE Decoder: [wan_alpha_2.1_vae_alpha_channel.safetensors.safetensors](https://huggingface.co/htdong/Wan-Alpha_ComfyUI/blob/main/wan_alpha_2.1_vae_alpha_channel.safetensors.safetensors)
2. Copy the files into the `ComfyUI/models` folder and organize them as follows:
```
ComfyUI/models
├── diffusion_models
│ └── wan2.1_t2v_14B_fp16.safetensors
├── loras
│ ├── epoch-13-1500_changed.safetensors
│ └── lightx2v_T2V_14B_cfg_step_distill_v2_lora_rank64_bf16.safetensors
├── text_encoders
│ └── umt5_xxl_fp8_e4m3fn_scaled.safetensors
├── vae
│ ├── wan_alpha_2.1_vae_alpha_channel.safetensors.safetensors
│ └── wan_alpha_2.1_vae_rgb_channel.safetensors.safetensors
```
3. Copy this repo or [SaveVideoRGBA.py](SaveVideoRGBA.py) into the `ComfyUI/custom_nodes` folder.
## 🤝 Acknowledgements
- [Wan-Alpha](https://github.com/WeChatCV/Wan-Alpha) for the alpha video generate
- [ComfyUI](https://github.com/comfyanonymous/ComfyUI) for the node workflow framework
+287
View File
@@ -0,0 +1,287 @@
from __future__ import annotations
from typing_extensions import override
from typing import Optional
from fractions import Fraction
from enum import Enum
import io
import json
import math
import av
import os
import random
import folder_paths
from comfy_api.latest._input import AudioInput, VideoInput
from comfy_api.latest import ComfyExtension, io, ui
from comfy_api.util import VideoComponents
class SaveVideoRGBA(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="SaveVideoRGBA",
display_name="Save Video (RGBA)",
category="image/animation",
inputs=[
io.Image.Input("images"),
io.Float.Input("fps", default=24.0, min=1.0, max=120.0, step=1.0),
io.String.Input("filename_prefix", default="video/ComfyUI", tooltip="The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes."),
io.Boolean.Input("only_preview", default=False),
io.Audio.Input("audio", optional=True),
],
outputs=[
# io.Video.Output("VIDEO")
],
hidden=[io.Hidden.unique_id],
is_output_node=True
)
@classmethod
def execute(self, images, fps, filename_prefix, only_preview, audio=None, **kwargs):
B, H, W, C = images.shape
has_alpha = C == 4
video = RGBAVideoFromComponents(
VideoComponents(
images=images,
audio=audio,
frame_rate=Fraction(fps),
)
)
width, height = video.get_dimensions()
results = list()
# 预览
if only_preview or has_alpha:
output_dir = folder_paths.get_temp_directory()
prefix_append = "ComfyUI_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5))
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
prefix_append,
output_dir,
width,
height
)
format = 'webm' if has_alpha else 'auto'
file = f"{filename}_{counter:05}_.{RGBAVideoContainer.get_extension(format)}"
video.save_to(
path=os.path.join(full_output_folder, file),
format=format,
codec='auto',
metadata=None,
)
results.append(ui.SavedResult(file, subfolder, io.FolderType.temp))
counter += 1
# 保存
if not only_preview:
output_dir = folder_paths.get_output_directory()
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
filename_prefix,
output_dir,
width,
height
)
format = 'mov' if has_alpha else 'auto'
codec = 'prores_ks' if has_alpha else 'auto'
file = f"{filename}_{counter:05}_.{RGBAVideoContainer.get_extension(format)}"
video.save_to(
path=os.path.join(full_output_folder, file),
format=format,
codec=codec,
metadata=None,
)
if not has_alpha:
results.append(ui.SavedResult(file, subfolder, io.FolderType.output))
counter += 1
return io.NodeOutput(ui=ui.PreviewVideo(results))
class NodeExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [
SaveVideoRGBA
]
async def comfy_entrypoint() -> NodeExtension:
return NodeExtension()
class RGBAVideoCodec(str, Enum):
AUTO = "auto"
H264 = "h264" # H.264 codec, no alpha channel
VP9 = "libvpx-vp9" # VP9 codec, supports alpha channel
PRORES = "prores_ks" # ProRes 4444, supports alpha channel
@classmethod
def as_input(cls) -> list[str]:
"""
Returns a list of codec names that can be used as node input.
"""
return [member.value for member in cls]
class RGBAVideoContainer(str, Enum):
AUTO = "auto"
MP4 = "mp4" # MP4 container
WEBM = "webm" # WebM container, supports VP9 with alpha channel
MOV = "mov" # QuickTime MOV, supports ProRes with alpha channel
@classmethod
def as_input(cls) -> list[str]:
"""
Returns a list of codec names that can be used as node input.
"""
return [member.value for member in cls]
@classmethod
def get_extension(cls, value) -> str:
"""
Returns the file extension for the container.
"""
if isinstance(value, str):
value = cls(value)
if value == RGBAVideoContainer.AUTO:
return "mp4"
elif value == RGBAVideoContainer.MP4:
return "mp4"
elif value == RGBAVideoContainer.WEBM:
return "webm"
elif value == RGBAVideoContainer.MOV:
return "mov"
return ""
class RGBAVideoFromComponents(VideoInput):
def __init__(self, components: VideoComponents):
self.__components = components
def get_components(self) -> VideoComponents:
return VideoComponents(
images=self.__components.images,
audio=self.__components.audio,
frame_rate=self.__components.frame_rate
)
def save_to(
self,
path: str,
format: RGBAVideoContainer = RGBAVideoContainer.AUTO,
codec: RGBAVideoCodec = RGBAVideoCodec.AUTO,
metadata: Optional[dict] = None
):
# Check if images have alpha channel (4 channels)
has_alpha = self.__components.images.shape[-1] == 4 if len(self.__components.images.shape) == 4 else False
# Determine format and codec based on alpha channel
if has_alpha:
# For alpha channel support, use webm container with vp9 codec
if format == RGBAVideoContainer.AUTO:
format = RGBAVideoContainer.WEBM.value
if codec == RGBAVideoCodec.AUTO:
codec = RGBAVideoCodec.VP9.value
# Ensure format is string for comparison
format_str = format.value if isinstance(format, RGBAVideoContainer) else format
codec_str = codec.value if isinstance(codec, RGBAVideoCodec) else codec
if format_str not in ['webm', 'mov']:
raise ValueError("Only WEBM and MOV formats support alpha channel")
else:
# Ensure format and codec are strings for comparison
format_str = format.value if isinstance(format, RGBAVideoContainer) else format
codec_str = codec.value if isinstance(codec, RGBAVideoCodec) else codec
if format != RGBAVideoContainer.AUTO and format_str not in ['mp4', 'webm', 'mov']:
raise ValueError("Supported formats: MP4, WEBM, MOV")
if codec != RGBAVideoCodec.AUTO and codec_str not in ['h264', 'libvpx-vp9', 'prores_ks']:
raise ValueError("Supported codecs: H264, VP9, ProRes")
# Set default codec if AUTO
if codec == RGBAVideoCodec.AUTO:
codec = RGBAVideoCodec.H264.value
codec_str = codec
# Ensure codec_str is defined
if 'codec_str' not in locals():
codec_str = codec.value if isinstance(codec, RGBAVideoCodec) else codec
# Prepare options based on format
options = {}
if format_str in ['mp4', 'mov']:
options['movflags'] = 'use_metadata_tags'
# Determine the format string for av.open
output_format = None if format == RGBAVideoContainer.AUTO else format_str
with av.open(path, mode='w', format=output_format, options=options) as output:
# Add metadata before writing any streams
if metadata is not None:
for key, value in metadata.items():
output.metadata[key] = json.dumps(value)
frame_rate = Fraction(round(self.__components.frame_rate * 1000), 1000)
# Create a video stream
video_stream = output.add_stream(codec_str, rate=frame_rate)
video_stream.width = self.__components.images.shape[2]
video_stream.height = self.__components.images.shape[1]
# Set pixel format based on codec and alpha channel
if has_alpha:
if codec_str in ['libvpx-vp9']:
video_stream.pix_fmt = 'yuva420p' # VP9 with alpha
elif codec_str in ['prores_ks']:
video_stream.pix_fmt = 'yuva444p10le' # ProRes 4444
else:
video_stream.pix_fmt = 'yuva420p' # Default alpha format
else:
if codec_str in ['h264']:
video_stream.pix_fmt = 'yuv420p'
elif codec_str in ['libvpx-vp9']:
video_stream.pix_fmt = 'yuv420p'
else:
video_stream.pix_fmt = 'yuv420p'
# Create an audio stream
audio_sample_rate = 1
audio_stream: Optional[av.AudioStream] = None
if self.__components.audio:
audio_sample_rate = int(self.__components.audio['sample_rate'])
audio_codec = 'libopus' if format_str == 'webm' else 'aac'
audio_stream = output.add_stream(audio_codec, rate=audio_sample_rate)
# Encode video
for i, frame in enumerate(self.__components.images):
img = (frame * 255).clamp(0, 255).byte().cpu().numpy()
if has_alpha:
# Create frame with alpha channel (H, W, 4)
frame = av.VideoFrame.from_ndarray(img, format='rgba')
frame = frame.reformat(format=video_stream.pix_fmt)
else:
# Create frame without alpha channel (H, W, 3)
frame = av.VideoFrame.from_ndarray(img, format='rgb24')
frame = frame.reformat(format=video_stream.pix_fmt)
packet = video_stream.encode(frame)
output.mux(packet)
# Flush video
packet = video_stream.encode(None)
output.mux(packet)
if audio_stream and self.__components.audio:
waveform = self.__components.audio['waveform']
waveform = waveform[:, :,
:math.ceil((audio_sample_rate / frame_rate) * self.__components.images.shape[0])]
frame = av.AudioFrame.from_ndarray(waveform.movedim(2, 1).reshape(1, -1).float().numpy(), format='flt',
layout='mono' if waveform.shape[1] == 1 else 'stereo')
frame.sample_rate = audio_sample_rate
frame.pts = 0
output.mux(audio_stream.encode(frame))
# Flush encoder
output.mux(audio_stream.encode(None))
+1
View File
@@ -0,0 +1 @@
from .SaveVideoRGBA import *
+833
View File
@@ -0,0 +1,833 @@
{
"id": "c265fdcc-11cd-4a1d-9a0d-bc37c45667d6",
"revision": 0,
"last_node_id": 79,
"last_link_id": 164,
"nodes": [
{
"id": 38,
"type": "CLIPLoader",
"pos": [
-117.83563995361328,
294.0386047363281
],
"size": [
390,
106
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "CLIP",
"type": "CLIP",
"slot_index": 0,
"links": [
117,
118
]
}
],
"title": "Load Text Encoder",
"properties": {
"Node name for S&R": "CLIPLoader"
},
"widgets_values": [
"umt5_xxl_fp8_e4m3fn_scaled.safetensors",
"wan",
"default"
]
},
{
"id": 40,
"type": "EmptyHunyuanLatentVideo",
"pos": [
463.4875793457031,
657.9263305664062
],
"size": [
315,
130
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"slot_index": 0,
"links": [
91
]
}
],
"properties": {
"Node name for S&R": "EmptyHunyuanLatentVideo"
},
"widgets_values": [
832,
480,
33,
1
]
},
{
"id": 3,
"type": "KSampler",
"pos": [
863,
187
],
"size": [
315,
262
],
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 95
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 46
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 52
},
{
"name": "latent_image",
"type": "LATENT",
"link": 91
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"slot_index": 0,
"links": [
35,
108
]
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
129212191626761,
"randomize",
4,
1,
"uni_pc",
"simple",
1
]
},
{
"id": 48,
"type": "ModelSamplingSD3",
"pos": [
572.943359375,
51.24325180053711
],
"size": [
210,
58
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 144
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"slot_index": 0,
"links": [
95
]
}
],
"properties": {
"Node name for S&R": "ModelSamplingSD3"
},
"widgets_values": [
5
]
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
365.5584411621094,
198.10687255859375
],
"size": [
422.84503173828125,
164.31304931640625
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 118
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
46
]
}
],
"title": "CLIP Text Encode (Positive Prompt)",
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"Medium shot. A little girl holds a bubble wand and blows out colorful bubbles that float and pop in the air. The background of this video is transparent. Realistic style."
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 51,
"type": "VAELoader",
"pos": [
867.8726196289062,
522.9005126953125
],
"size": [
306.36004638671875,
58
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "VAE",
"type": "VAE",
"slot_index": 0,
"links": [
107
]
}
],
"properties": {
"Node name for S&R": "VAELoader"
},
"widgets_values": [
"wan_alpha_2.1_vae_alpha_channel.safetensors.safetensors"
]
},
{
"id": 39,
"type": "VAELoader",
"pos": [
873.0031127929688,
51.24325180053711
],
"size": [
306.36004638671875,
58
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "VAE",
"type": "VAE",
"slot_index": 0,
"links": [
76
]
}
],
"properties": {
"Node name for S&R": "VAELoader"
},
"widgets_values": [
"wan_alpha_2.1_vae_rgb_channel.safetensors.safetensors"
]
},
{
"id": 37,
"type": "UNETLoader",
"pos": [
-415.37969970703125,
51.24325180053711
],
"size": [
346.7470703125,
82
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"slot_index": 0,
"links": [
142
]
}
],
"title": "Load Wan 2.1 t2v 14B",
"properties": {
"Node name for S&R": "UNETLoader"
},
"widgets_values": [
"wan2.1_t2v_14B_fp16.safetensors",
"default"
]
},
{
"id": 65,
"type": "LoraLoaderModelOnly",
"pos": [
267.7607421875,
51.24325180053711
],
"size": [
270,
82
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 143
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
144
]
}
],
"properties": {
"Node name for S&R": "LoraLoaderModelOnly"
},
"widgets_values": [
"epoch-13-1500_changed.safetensors",
1
]
},
{
"id": 59,
"type": "LoraLoaderModelOnly",
"pos": [
-38.43362045288086,
51.24325180053711
],
"size": [
270,
82
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 142
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
143
]
}
],
"properties": {
"Node name for S&R": "LoraLoaderModelOnly"
},
"widgets_values": [
"lightx2v_T2V_14B_cfg_step_distill_v2_lora_rank64_bf16.safetensors",
1
]
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
359.2072448730469,
406.81744384765625
],
"size": [
425.27801513671875,
180.6060791015625
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 117
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
52
]
}
],
"title": "CLIP Text Encode (Negative Prompt) Useless s",
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
""
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 52,
"type": "VAEDecode",
"pos": [
1220.6512451171875,
530.7353515625
],
"size": [
210,
46
],
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 108
},
{
"name": "vae",
"type": "VAE",
"link": 107
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"slot_index": 0,
"links": [
163
]
}
],
"title": "Alpha VAE Decode",
"properties": {
"Node name for S&R": "VAEDecode"
},
"widgets_values": []
},
{
"id": 8,
"type": "VAEDecode",
"pos": [
1220.984130859375,
51.24325180053711
],
"size": [
210,
46
],
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 35
},
{
"name": "vae",
"type": "VAE",
"link": 76
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"slot_index": 0,
"links": [
164
]
}
],
"title": "RGB VAE Decode",
"properties": {
"Node name for S&R": "VAEDecode"
},
"widgets_values": []
},
{
"id": 74,
"type": "JoinImageWithAlpha",
"pos": [
1464.0049568109648,
205.78294262667404
],
"size": [
180.52341319729067,
46
],
"flags": {},
"order": 15,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 164
},
{
"name": "alpha",
"type": "MASK",
"link": 158
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
162
]
}
],
"properties": {
"Node name for S&R": "JoinImageWithAlpha"
}
},
{
"id": 75,
"type": "InvertMask",
"pos": [
1488,
374.66456757897765
],
"size": [
140,
26
],
"flags": {},
"order": 14,
"mode": 0,
"inputs": [
{
"name": "mask",
"type": "MASK",
"link": 159
}
],
"outputs": [
{
"name": "MASK",
"type": "MASK",
"links": [
158
]
}
],
"properties": {
"Node name for S&R": "InvertMask"
}
},
{
"id": 76,
"type": "ImageToMask",
"pos": [
1465.4226047466786,
482.26601727075456
],
"size": [
210,
58
],
"flags": {},
"order": 13,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 163
}
],
"outputs": [
{
"name": "MASK",
"type": "MASK",
"links": [
159
]
}
],
"properties": {
"Node name for S&R": "ImageToMask"
},
"widgets_values": [
"red"
]
},
{
"id": 79,
"type": "SaveVideoRGBA",
"pos": [
1720.5411964998127,
51.24325180053711
],
"size": [
441.01432163205754,
896.260567585984
],
"flags": {},
"order": 16,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 162
},
{
"name": "audio",
"shape": 7,
"type": "AUDIO",
"link": null
}
],
"outputs": [
{
"name": "VIDEO",
"type": "VIDEO",
"links": null
}
],
"properties": {
"Node name for S&R": "SaveVideoRGBA"
},
"widgets_values": [
24,
"video/ComfyUI",
false
]
}
],
"links": [
[
35,
3,
0,
8,
0,
"LATENT"
],
[
46,
6,
0,
3,
1,
"CONDITIONING"
],
[
52,
7,
0,
3,
2,
"CONDITIONING"
],
[
76,
39,
0,
8,
1,
"VAE"
],
[
91,
40,
0,
3,
3,
"LATENT"
],
[
95,
48,
0,
3,
0,
"MODEL"
],
[
107,
51,
0,
52,
1,
"VAE"
],
[
108,
3,
0,
52,
0,
"LATENT"
],
[
117,
38,
0,
7,
0,
"CLIP"
],
[
118,
38,
0,
6,
0,
"CLIP"
],
[
142,
37,
0,
59,
0,
"MODEL"
],
[
143,
59,
0,
65,
0,
"MODEL"
],
[
144,
65,
0,
48,
0,
"MODEL"
],
[
158,
75,
0,
74,
1,
"MASK"
],
[
159,
76,
0,
75,
0,
"MASK"
],
[
162,
74,
0,
79,
0,
"IMAGE"
],
[
163,
52,
0,
76,
0,
"IMAGE"
],
[
164,
8,
0,
74,
0,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.5949345628810089,
"offset": [
657.1153588041857,
464.13758186017674
]
},
"frontendVersion": "1.28.6",
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,
"VHS_KeepIntermediate": true
},
"version": 0.4
}