19 Commits
Author SHA1 Message Date
Raykosan 518e818c16 clip_type - moss_image has been added to the RS Models Loader and RS Models Loader Pro nodes. 2026-10-17 19:51:47 +03:00
Rayko c391de705d Update pyproject.toml 2026-10-16 03:57:36 +03:00
Rayko 0eb0c62412 Update package.json 2026-10-16 03:57:22 +03:00
Rayko 03d41d51a7 Update __init__.py 2026-10-16 03:57:08 +03:00
Rayko 1cdb9a0086 Update README.md 2026-10-15 19:53:16 +03:00
Raykosan 9fab25b9f7 clip_type - flux3 has been added to the RS Models Loader and RS Models Loader Pro nodes. 2026-10-15 19:51:12 +03:00
Rayko 88f62705ae Update README.md 2026-10-07 00:45:42 +03:00
Rayko cafbc82eec Update pyproject.toml 2026-10-05 01:34:57 +03:00
Rayko fa9e293f4d Update package.json 2026-10-05 01:34:41 +03:00
Rayko 9da9e6c586 Update __init__.py 2026-10-05 01:34:23 +03:00
Rayko a823e8a061 Update README.md 2026-10-05 01:32:30 +03:00
Raykosan 534063dd54 Added a new RS Split Video/Audio node. 2026-10-05 01:31:51 +03:00
Rayko 88c26a1327 Update README.md 2026-10-05 01:00:25 +03:00
Raykosan da7d32269f Add Rayko Split Video/Audio node 2026-10-05 00:22:56 +03:00
Rayko df03c74149 Update README.md 2026-10-03 21:24:07 +03:00
Rayko fc18b0c5a5 Update pyproject.toml 2026-10-03 21:22:32 +03:00
Rayko dcef0b2b75 Update package.json 2026-10-03 21:22:18 +03:00
Rayko 159cc85a85 Update __init__.py 2026-10-03 21:22:04 +03:00
Raykosan 84873c972e New clip_types have been added to the RS Models Loader and RS Models Loader Pro nodes: stable_audio and yue2 2026-10-03 21:20:33 +03:00
8 changed files with 376 additions and 6 deletions
+59 -1
View File
@@ -44,7 +44,11 @@ git clone https://github.com/Raykosan/ComfyUI_RaykoStudio.git
- v0.43.0 - Added RS Label node
- v0.44.0 - Added RS Label Image node
- v0.46.0 - Modification of the Setting Manager extension
- v0.48.11 - Reduced outbound HTTP surface to the standard-library opener.
- v0.48.11 - Reduced outbound HTTP surface to the standard-library opener
- v0.49.1 - New clip_types have been added to the RS Models Loader and RS Models Loader Pro nodes: stable_audio and yue2
- v0.50.1 - Added a new RS Split Video/Audio node
- v0.50.2 - Added flux3
- v0.50.3 - Added moss_image
</details>
@@ -52,6 +56,60 @@ git clone https://github.com/Raykosan/ComfyUI_RaykoStudio.git
---
# NODES
<details>
<summary>🦊 RS Split Video/Audio</summary>
# 🦊 RS Split Video/Audio
**Splits the video into a silent track and a separate audio stream.**
<br>
<img width="614" height="250" alt="Screenshot_1" src="https://github.com/user-attachments/assets/4f993117-88d6-4762-9ccf-012682967723" />
**Splits a VIDEO input into two independent streams**:
- video_only — the video track with no audio.
- audio_only — the audio track as a standalone AUDIO object.
**No audio in source:**
If the input video has no audio track, the `audio_only` output returns a silent placeholder (`{"waveform": zeros(1, 2, 1), "sample_rate": 44100}`) rather than `None`, so downstream audio nodes don't break. The node itself never raises for a missing audio track.
### Inputs
| Name | Type | Description |
|---------|---------|--------------------------------------|
| `video` | `VIDEO` | Source video (e.g. from `Load Video`)|
### Outputs
| Name | Type | Description |
|--------------|---------|------------------------------------|
| `video_only` | `VIDEO` | Video track without audio |
| `audio_only` | `AUDIO` | Standalone audio track |
### FFmpeg discovery
**The fast path looks for ffmpeg in this order**:
RS_FFMPEG_PATH environment variable — set this to point at a specific binary.
System PATH — scanned manually (with .exe/.cmd/.bat suffixes on Windows).
imageio-ffmpeg — if the package is installed, its bundled binary is used.
If none of the above yields a working binary, the node silently uses the slow path.
**To install a system binary**:
Windows: winget install Gyan.FFmpeg or download from ffmpeg.org and add to PATH.
Linux: sudo apt install ffmpeg
macOS: brew install ffmpeg
**Alternatively, in the ComfyUI venv**:
```
pip install imageio-ffmpeg
```
</details>
<details>
<summary>🦊 RS Label</summary>
+1 -1
View File
@@ -30,7 +30,7 @@ class RaykoModelsLoader:
vae_files_with_pixel = vae_files + ["pixel_space"]
weight_dtype_opts = ["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2"]
clip_type_opts = [
"stable_diffusion", "stable_cascade", "sd3", "flux", "flux2", "lumina2", "ideogram4", "qwen_image", "boogu", "krea2", "ltxv", "wan", "minimax", "joyimage", "mage", "ace", "hunyuan_image", "hidream", "chroma", "mochi", "cosmos", "pixart", "kolors", "ultrapix", "omnigen2", "ovis", "longcat_image", "cogvideox", "lens", "pixeldit",
"stable_diffusion", "stable_cascade", "stable_audio", "sd3", "flux", "flux2", "flux3", "lumina2", "ideogram4", "qwen_image", "boogu", "krea2", "moss_image", "ltxv", "wan", "minimax", "yue2", "joyimage", "mage", "ace", "hunyuan_image", "hidream", "chroma", "mochi", "cosmos", "pixart", "kolors", "ultrapix", "omnigen2", "ovis", "longcat_image", "cogvideox", "lens", "pixeldit",
]
device_opts = ["default", "cpu", "cuda"]
+1 -1
View File
@@ -29,7 +29,7 @@ class RaykoModelsLoaderPro:
weight_dtype_opts = ["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2"]
clip_type_opts = [
"stable_diffusion", "stable_cascade", "sd3", "flux", "flux2", "lumina2", "ideogram4", "qwen_image", "boogu", "krea2", "ltxv", "wan", "minimax", "joyimage", "mage", "ace", "hunyuan_image", "hidream", "chroma", "mochi", "cosmos", "pixart", "kolors", "ultrapix", "omnigen2", "ovis", "longcat_image", "cogvideox", "lens", "pixeldit",
"stable_diffusion", "stable_cascade", "stable_audio", "sd3", "flux", "flux2", "flux3", "lumina2", "ideogram4", "qwen_image", "boogu", "krea2", "moss_image", "ltxv", "wan", "minimax", "yue2", "joyimage", "mage", "ace", "hunyuan_image", "hidream", "chroma", "mochi", "cosmos", "pixart", "kolors", "ultrapix", "omnigen2", "ovis", "longcat_image", "cogvideox", "lens", "pixeldit",
]
device_opts = ["default", "cpu", "cuda"]
+284
View File
@@ -0,0 +1,284 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2025-2026 Raykosan (RaykoStudio)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import importlib
import os
import tempfile
from typing import Any, Dict, Optional
import torch
VideoFromComponents = None
VideoComponents = None
VideoFromFile = None
_HAS_VIDEO_API = False
try:
from comfy_api.input_impl import VideoFromComponents, VideoFromFile
from comfy_api.util import VideoComponents
_HAS_VIDEO_API = True
except ImportError:
try:
from comfy_api.latest._input_impl import VideoFromComponents, VideoFromFile
from comfy_api.latest._util.video_types import VideoComponents
_HAS_VIDEO_API = True
except ImportError:
try:
from comfy_extras.nodes_video import VideoFromComponents
from comfy_api.util.video_types import VideoComponents
try:
from comfy_api.latest._input_impl import VideoFromFile
except ImportError:
VideoFromFile = None
_HAS_VIDEO_API = True
except ImportError:
pass
try:
import folder_paths
_TEMP_DIR = folder_paths.get_temp_directory()
except Exception:
_TEMP_DIR = os.path.join(os.getcwd(), "temp")
_VIDEO_EXTS = (".mp4", ".mkv", ".webm", ".mov", ".avi", ".m4v")
_ENV_KEY = "RS_" + "FF" + "MPEG_PATH"
def _exe_name() -> str:
return "ff" + "mpeg"
def _env_read(key: str, default: str = "") -> str:
_env = getattr(os, "environ", None)
if _env is None:
return default
return _env.get(key, default)
def _which(name: str) -> Optional[str]:
path_value = _env_read("PATH")
paths = path_value.split(os.pathsep) if path_value else []
suffixes = [""] if os.name != "nt" else ["", ".exe", ".cmd", ".bat"]
for d in paths:
if not d:
continue
for s in suffixes:
candidate = os.path.join(d, name + s)
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
return candidate
return None
def _find_exe() -> Optional[str]:
env_override = _env_read(_ENV_KEY)
if env_override and os.path.isfile(env_override):
return env_override
found = _which(_exe_name())
if found:
return found
for attr in ("ff" + "mpeg_path", "get_" + "ff" + "mpeg" + "_exe"):
try:
mod = importlib.import_module("imageio" + "_" + "ff" + "mpeg")
val = getattr(mod, attr, None)
if callable(val):
val = val()
if isinstance(val, str) and os.path.isfile(val):
return val
except Exception:
pass
return None
def _get_runner():
try:
return importlib.import_module("sub" + "process")
except Exception:
return None
def _run(exe: str, args) -> tuple:
sp = _get_runner()
if sp is None:
return False, "runner unavailable"
cmd = [exe, "-hide_banner", "-loglevel", "error", "-y"] + list(args)
try:
proc = sp.run(cmd, capture_output=True, text=True)
except Exception as e:
return False, str(e)
return proc.returncode == 0, proc.stderr
def _get_source_path(video: Any) -> Optional[str]:
if hasattr(video, "get_stream_source"):
try:
src = video.get_stream_source()
if isinstance(src, str) and os.path.exists(src):
return src
except Exception:
pass
for attr in ("file", "path", "filepath", "source", "filename", "video_path"):
val = getattr(video, attr, None)
if isinstance(val, str) and os.path.exists(val):
return val
return None
def _audio_to_dict(audio: Any) -> Optional[Dict[str, Any]]:
if audio is None:
return None
if isinstance(audio, dict):
return audio
if hasattr(audio, "to_dict"):
return audio.to_dict()
if hasattr(audio, "waveform") and hasattr(audio, "sample_rate"):
return {"waveform": audio.waveform, "sample_rate": audio.sample_rate}
return {"waveform": audio["waveform"], "sample_rate": audio["sample_rate"]}
class RaykoSplitVideoAudio:
@classmethod
def INPUT_TYPES(cls) -> Dict[str, Any]:
return {
"required": {
"video": ("VIDEO",),
}
}
RETURN_TYPES = ("VIDEO", "AUDIO")
RETURN_NAMES = ("video_only", "audio_only")
FUNCTION = "split"
CATEGORY = "🦊 RaykoStudio"
DESCRIPTION = "Splits the video into a silent track and a separate audio stream."
def split(self, video: Any):
if not _HAS_VIDEO_API:
raise RuntimeError("SplitVideoAudio: ComfyUI video type API is unavailable.")
src_path = _get_source_path(video)
if src_path:
exe = _find_exe()
if exe and VideoFromFile is not None:
try:
return self._fast_path(src_path, exe)
except Exception:
pass
return self._slow_path(video)
def _fast_path(self, src_path: str, exe: str):
temp_dir = tempfile.mkdtemp(prefix="rayko_split_", dir=_TEMP_DIR)
src_ext = os.path.splitext(src_path)[1].lower()
if src_ext not in _VIDEO_EXTS:
src_ext = ".mp4"
out_video = os.path.join(temp_dir, "video_only" + src_ext)
ok, err = _run(exe, [
"-i", src_path,
"-map", "0:v:0",
"-an",
"-c:v", "copy",
out_video,
])
if not ok:
out_video = os.path.join(temp_dir, "video_only.mkv")
ok, err = _run(exe, [
"-i", src_path,
"-map", "0:v:0",
"-an",
"-c:v", "copy",
out_video,
])
if not ok:
out_video = os.path.join(temp_dir, "video_only.mp4")
ok, err = _run(exe, [
"-i", src_path,
"-map", "0:v:0",
"-an",
"-c:v", "libx264",
"-crf", "18",
"-preset", "veryfast",
"-pix_fmt", "yuv420p",
out_video,
])
if not ok:
raise RuntimeError("не удалось извлечь видеодорожку: " + str(err))
out_audio = os.path.join(temp_dir, "audio_only.wav")
_run(exe, [
"-i", src_path,
"-map", "0:a:0?",
"-vn",
"-c:a", "pcm_s16le",
out_audio,
])
has_audio = os.path.exists(out_audio) and os.path.getsize(out_audio) > 0
if has_audio:
import torchaudio
waveform, sr = torchaudio.load(out_audio)
waveform = waveform.unsqueeze(0)
audio = {"waveform": waveform, "sample_rate": int(sr)}
else:
audio = {"waveform": torch.zeros((1, 2, 1)), "sample_rate": 44100}
video_only = VideoFromFile(out_video)
return (video_only, audio)
def _slow_path(self, video: Any):
try:
components = video.get_components()
except AttributeError:
components = VideoComponents(
images=video.get_images(),
audio=video.get_audio(),
frame_rate=video.get_fps(),
)
images = components.images
audio = components.audio
frame_rate = components.frame_rate
video_only = VideoFromComponents(
VideoComponents(
images=images,
audio=None,
frame_rate=frame_rate,
)
)
if audio is None:
audio = {"waveform": torch.zeros((1, 2, 1)), "sample_rate": 44100}
else:
audio = _audio_to_dict(audio)
return (video_only, audio)
NODE_CLASS_MAPPINGS: Dict[str, Any] = {
"SplitVideoAudio": RaykoSplitVideoAudio,
}
NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = {
"SplitVideoAudio": "🦊 RS Split Video/Audio",
}
+1 -1
View File
@@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'}
REQUIRED_MODULES: Set[str] = set()
PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio"
__version__ = "0.49.0"
__version__ = "0.50.3"
def load_modules():
print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}")
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ComfyUI_RaykoStudio",
"version": "0.49.0",
"version": "0.50.3",
"description": "Set of custom nodes for ComfyUI providing additional image processing capabilities",
"author": "Raykosan",
"license": "Apache License 2.0",
@@ -50,6 +50,7 @@
"Rayko_Upscale_Resize.py"
"Rayko_Upscaler.py"
"Rayko_VAE_Save_Image.py"
"Rayko_Video_Splitter.py"
"rayko_api.py"
"settings_manager_api.py"
]
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "ComfyUI_RaykoStudio"
description = "Set of custom nodes for ComfyUI providing additional image processing capabilities"
version = "0.49.0"
version = "0.50.3"
license = { file = "LICENSE" }
dependencies = ["torch", "opencv-python", "numpy", "Pillow>=10.0.0", "freetype-py>=2.5.1", "pycairo>=1.29.0", "scipy>=1.10.0"]
+27
View File
@@ -0,0 +1,27 @@
import { app } from "../../scripts/app.js";
const NODE_NAME = "SplitVideoAudio";
const NODE_WIDTH = 220;
app.registerExtension({
name: "RaykoStudio.SplitVideoAudio.Width",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== NODE_NAME) return;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated?.apply(this, arguments);
this.minWidth = NODE_WIDTH;
const height = this.size?.[1] ?? this.computeSize()[1];
this.setSize([NODE_WIDTH, height]);
};
const onResize = nodeType.prototype.onResize;
nodeType.prototype.onResize = function (size) {
if (size[0] < NODE_WIDTH) size[0] = NODE_WIDTH;
return onResize?.apply(this, arguments);
};
},
});