diff --git a/Rayko_Video_Splitter.py b/Rayko_Video_Splitter.py new file mode 100644 index 0000000..cfcc225 --- /dev/null +++ b/Rayko_Video_Splitter.py @@ -0,0 +1,275 @@ +# 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 _which(name: str) -> Optional[str]: + paths = os.environ.get("PATH", "").split(os.pathsep) + 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 = os.environ.get(_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: API видео-типов ComfyUI недоступен.") + + 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", +} \ No newline at end of file diff --git a/__init__.py b/__init__.py index c7c555a..dd25b84 100644 --- a/__init__.py +++ b/__init__.py @@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'} REQUIRED_MODULES: Set[str] = set() PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio" -__version__ = "0.49.1" +__version__ = "0.50.0" def load_modules(): print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}") diff --git a/package.json b/package.json index a135e35..94d91db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ComfyUI_RaykoStudio", - "version": "0.49.1", + "version": "0.50.0", "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" ] diff --git a/pyproject.toml b/pyproject.toml index f6808d4..b8b75a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ComfyUI_RaykoStudio" description = "Set of custom nodes for ComfyUI providing additional image processing capabilities" -version = "0.49.1" +version = "0.50.0" 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"] diff --git a/web/rs_split_video_audio_width.js b/web/rs_split_video_audio_width.js new file mode 100644 index 0000000..7bd76e6 --- /dev/null +++ b/web/rs_split_video_audio_width.js @@ -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); + }; + }, +}); \ No newline at end of file