Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9862906fae | ||
|
|
5cc28c6fb2 | ||
|
|
4bb484f9a9 | ||
|
|
be2a373c31 | ||
|
|
6408e014a3 | ||
|
|
6b0b6c16d7 | ||
|
|
db28fc5a7e | ||
|
|
4ea7928657 | ||
|
|
c04fa2fb57 | ||
|
|
07ba5d2bb6 | ||
|
|
a9cfa35bd5 | ||
|
|
f8b75b2a29 | ||
|
|
952bdb34c7 | ||
|
|
9afdeb6674 | ||
|
|
0fd1348c43 | ||
|
|
8bc15a1716 | ||
|
|
9914c3ded0 | ||
|
|
c7d208714c | ||
|
|
f26c48ea5b | ||
|
|
b1d52082da | ||
|
|
2700fd3695 | ||
|
|
518e818c16 | ||
|
|
c391de705d | ||
|
|
0eb0c62412 | ||
|
|
03d41d51a7 | ||
|
|
1cdb9a0086 | ||
|
|
9fab25b9f7 | ||
|
|
88f62705ae | ||
|
|
cafbc82eec | ||
|
|
fa9e293f4d | ||
|
|
9da9e6c586 | ||
|
|
a823e8a061 | ||
|
|
534063dd54 | ||
|
|
88c26a1327 | ||
|
|
da7d32269f | ||
|
|
df03c74149 | ||
|
|
fc18b0c5a5 | ||
|
|
dcef0b2b75 | ||
|
|
159cc85a85 | ||
|
|
84873c972e | ||
|
|
1d00f7109b | ||
|
|
dd65175c96 | ||
|
|
36782244ed | ||
|
|
4dc9b750a7 | ||
|
|
2c94cc574c |
+2
-1
@@ -8,7 +8,8 @@ preset_models/
|
||||
presets/
|
||||
prompts/
|
||||
rayko_lora_data/
|
||||
styles/presets/
|
||||
styles/
|
||||
system_prompts/
|
||||
ComfyUI-Custom-Node-Manager/
|
||||
python
|
||||
|
||||
|
||||
@@ -43,7 +43,15 @@ git clone https://github.com/Raykosan/ComfyUI_RaykoStudio.git
|
||||
- v0.42.1 - Changes in the interfaces of some nodes
|
||||
- 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.46.0 - Modification of the Setting Manager extension
|
||||
- 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
|
||||
- v0.51.0 - Added the RS Load System Prompt node
|
||||
- v0.51.1 - Added fibo2
|
||||
- v0.51.2 - Modification of the RS Load System Prompt node
|
||||
|
||||
</details>
|
||||
|
||||
@@ -51,6 +59,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>
|
||||
|
||||
@@ -268,6 +330,59 @@ In any case, you can always paste the path using the keyboard shortcut Ctrl+V.
|
||||
| `number_of_files` | INT | The total number of files found matching the filter in the directory. |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>🦊 RS Load System Prompt</summary>
|
||||
|
||||
# 🦊 RS Load System Prompt
|
||||
**A ComfyUI custom node that loads, edits, and saves system prompts directly from `.txt` files — with a searchable tree dropdown, folder hierarchy, live file list refresh, and built-in save/delete functionality.**
|
||||
|
||||
<img width="630" height="865" alt="Screenshot_1" src="https://github.com/user-attachments/assets/5430770e-5809-4605-b389-fa29a259c9bb" />
|
||||
|
||||
### 🔥 Features
|
||||
**Folder-based prompts** — every `.txt` file inside `system_prompts/` becomes a selectable prompt.
|
||||
**Tree dropdown with subfolders** — nested directories are shown as expandable folders.
|
||||
**Search** — filter by any part of the path (file name or folder).
|
||||
**Live refresh** — the file list is re-scanned every time the dropdown opens. No need to reload ComfyUI after adding a new `.txt`.
|
||||
**Editable system prompt** — select a file and its contents load into a large textarea where you can edit them.
|
||||
**Save edited prompts** — save your changes back to the existing file or create a new one. `.txt` extension is added automatically.
|
||||
**Delete files** — remove prompt files directly from the dropdown (with a confirmation dialog).
|
||||
**Reload from file** — revert your textarea to the on-disk version if you made a mistake.
|
||||
**User prompt textarea** — a separate multiline input for additional prompt text, passed through as a distinct output.
|
||||
**Unsaved changes indicator** — a `*` appears in the selection button when your textarea differs from the saved file.
|
||||
**Safe by default** — path traversal protection, `PermissionError` / `UnicodeDecodeError` handling, BOM-aware UTF-8 reading.
|
||||
**Compact, polished UI** — dark-themed modals for save/confirm, centered labels, and a clean layout.
|
||||
|
||||
### 🪛 Usage
|
||||
- Add the node 🦊 RS Load System Prompt from the 🦊 RaykoStudio category.
|
||||
- Click LOAD SYSTEM PROMPT (below the system prompt textarea).
|
||||
- Browse the tree or type into the search field.
|
||||
- Select a .txt file — its contents load into the SYSTEM PROMPT textarea.
|
||||
- Edit the text if needed.
|
||||
- Click 💾 to save:
|
||||
- Keep the same name to overwrite the current file.
|
||||
- Change the name to save as a new file (the old one stays untouched).
|
||||
- Optionally fill the USER PROMPT textarea with extra text.
|
||||
- Connect the outputs:
|
||||
- system_prompt — the contents of the SYSTEM PROMPT textarea.
|
||||
- prompt — text from the USER PROMPT textarea.
|
||||
|
||||
### 📝 Adding new prompts
|
||||
Drop any .txt file into:
|
||||
`ComfyUI/custom_nodes/ComfyUI_RaykoStudio/system_prompts/`
|
||||
Subfolders are supported. The file list is refreshed automatically every time you open the dropdown — no ComfyUI restart or page reload needed.
|
||||
|
||||
### 💡 Note
|
||||
If the dropdown is already open and you add a file from outside ComfyUI, click 🔄 Refresh inside the dropdown to re-scan.
|
||||
|
||||
### 🔘 Buttons
|
||||
⟳ Reload the current file from disk. Asks for confirmation if there are unsaved changes.
|
||||
💾 Open the save dialog. Pre-filled with the current file name (selected). .txt is added automatically if missing.
|
||||
🔄 Refresh (in dropdown) Re-scan the system_prompts/ folder without closing the dropdown.
|
||||
❌ (in dropdown) Delete a prompt file (with confirmation).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>🦊 RS Image-Prompt</summary>
|
||||
|
||||
|
||||
@@ -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", "fibo2", "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"]
|
||||
|
||||
@@ -103,4 +103,4 @@ class RaykoModelsLoader:
|
||||
return (model, clip, vae)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"RaykoModelsLoader": RaykoModelsLoader}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"RaykoModelsLoader": "🦊 RS Models Loader"}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"RaykoModelsLoader": "🦊 RS Models Loader"}
|
||||
|
||||
@@ -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", "fibo2", "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"]
|
||||
|
||||
@@ -106,4 +106,4 @@ class RaykoModelsLoaderPro:
|
||||
return (model1, model2, clip, vae1, vae2)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"RaykoModelsLoaderPro": RaykoModelsLoaderPro}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"RaykoModelsLoaderPro": "🦊 RS Models Loader Pro"}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"RaykoModelsLoaderPro": "🦊 RS Models Loader Pro"}
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
# 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 os
|
||||
import server
|
||||
from aiohttp import web
|
||||
|
||||
|
||||
NODE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
PROMPT_DIR = os.path.join(NODE_DIR, "system_prompts")
|
||||
|
||||
ROUTE_GET_LIST = "/rs_system_prompt_dropdown/get_prompts"
|
||||
ROUTE_GET_CONTENT = "/rs_system_prompt_dropdown/get_prompt_content"
|
||||
ROUTE_SAVE = "/rs_system_prompt_dropdown/save_prompt"
|
||||
|
||||
|
||||
def ensure_prompt_dir():
|
||||
try:
|
||||
os.makedirs(PROMPT_DIR, exist_ok=True)
|
||||
return os.path.isdir(PROMPT_DIR)
|
||||
except OSError as error:
|
||||
print(f"[RSLoadSystemPrompt] Cannot create prompt dir '{PROMPT_DIR}': {error}")
|
||||
return False
|
||||
|
||||
|
||||
def _scan_prompts():
|
||||
if not ensure_prompt_dir():
|
||||
return []
|
||||
|
||||
files = []
|
||||
for root, _, filenames in os.walk(PROMPT_DIR):
|
||||
for filename in filenames:
|
||||
if filename.lower().endswith(".txt"):
|
||||
full_path = os.path.join(root, filename)
|
||||
relative_path = os.path.relpath(full_path, PROMPT_DIR)
|
||||
files.append(relative_path.replace(os.sep, "/"))
|
||||
|
||||
files.sort(key=str.casefold)
|
||||
return files
|
||||
|
||||
|
||||
def resolve_prompt_path(prompt_file, must_exist=False):
|
||||
if not prompt_file:
|
||||
return None
|
||||
try:
|
||||
rel = prompt_file.replace("\\", "/").replace("/", os.sep)
|
||||
file_path = os.path.abspath(os.path.join(PROMPT_DIR, rel))
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
|
||||
prompt_root = os.path.abspath(PROMPT_DIR)
|
||||
if not file_path.startswith(prompt_root + os.sep):
|
||||
return None
|
||||
if file_path == prompt_root:
|
||||
return None
|
||||
if must_exist and not os.path.isfile(file_path):
|
||||
return None
|
||||
return file_path
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get(ROUTE_GET_LIST)
|
||||
async def get_prompts_handler(request):
|
||||
return web.json_response({"files": _scan_prompts()})
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get(ROUTE_GET_CONTENT)
|
||||
async def get_prompt_content_handler(request):
|
||||
filename = request.query.get("file", "")
|
||||
file_path = resolve_prompt_path(filename, must_exist=True)
|
||||
if file_path is None:
|
||||
return web.json_response(
|
||||
{"ok": False, "error": "File not found or invalid path"},
|
||||
status=404,
|
||||
)
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8-sig") as file:
|
||||
content = file.read()
|
||||
return web.json_response({"ok": True, "content": content})
|
||||
except (OSError, UnicodeDecodeError) as error:
|
||||
return web.json_response(
|
||||
{"ok": False, "error": f"Error reading file: {error}"},
|
||||
status=500,
|
||||
)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.post(ROUTE_SAVE)
|
||||
async def save_prompt_handler(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
return web.json_response(
|
||||
{"ok": False, "error": "Invalid JSON body"},
|
||||
status=400,
|
||||
)
|
||||
|
||||
filename = (data or {}).get("filename", "")
|
||||
content = (data or {}).get("content", "")
|
||||
|
||||
if not isinstance(filename, str) or not isinstance(content, str):
|
||||
return web.json_response(
|
||||
{"ok": False, "error": "filename and content must be strings"},
|
||||
status=400,
|
||||
)
|
||||
|
||||
filename = filename.strip().replace("\\", "/")
|
||||
if not filename:
|
||||
return web.json_response(
|
||||
{"ok": False, "error": "Empty file name"},
|
||||
status=400,
|
||||
)
|
||||
|
||||
if not filename.lower().endswith(".txt"):
|
||||
filename += ".txt"
|
||||
|
||||
if not ensure_prompt_dir():
|
||||
return web.json_response(
|
||||
{"ok": False, "error": "Cannot access system_prompts directory"},
|
||||
status=500,
|
||||
)
|
||||
|
||||
file_path = resolve_prompt_path(filename)
|
||||
if file_path is None:
|
||||
return web.json_response(
|
||||
{"ok": False, "error": "Invalid path"},
|
||||
status=400,
|
||||
)
|
||||
|
||||
created = not os.path.exists(file_path)
|
||||
|
||||
try:
|
||||
parent = os.path.dirname(file_path)
|
||||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
except OSError as error:
|
||||
return web.json_response(
|
||||
{"ok": False, "error": f"Cannot create folder: {error}"},
|
||||
status=500,
|
||||
)
|
||||
|
||||
try:
|
||||
with open(file_path, "w", encoding="utf-8", newline="\n") as file:
|
||||
file.write(content)
|
||||
except OSError as error:
|
||||
return web.json_response(
|
||||
{"ok": False, "error": f"Error writing file: {error}"},
|
||||
status=500,
|
||||
)
|
||||
|
||||
rel_path = os.path.relpath(file_path, PROMPT_DIR).replace(os.sep, "/")
|
||||
return web.json_response(
|
||||
{"ok": True, "path": rel_path, "created": created}
|
||||
)
|
||||
|
||||
|
||||
ROUTE_DELETE = "/rs_system_prompt_dropdown/delete_prompt"
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.post(ROUTE_DELETE)
|
||||
async def delete_prompt_handler(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
return web.json_response(
|
||||
{"ok": False, "error": "Invalid JSON body"},
|
||||
status=400,
|
||||
)
|
||||
|
||||
filename = (data or {}).get("filename", "")
|
||||
if not isinstance(filename, str) or not filename.strip():
|
||||
return web.json_response(
|
||||
{"ok": False, "error": "Empty file name"},
|
||||
status=400,
|
||||
)
|
||||
|
||||
file_path = resolve_prompt_path(filename.strip(), must_exist=True)
|
||||
if file_path is None:
|
||||
return web.json_response(
|
||||
{"ok": False, "error": "File not found or invalid path"},
|
||||
status=404,
|
||||
)
|
||||
|
||||
try:
|
||||
os.remove(file_path)
|
||||
except OSError as error:
|
||||
return web.json_response(
|
||||
{"ok": False, "error": f"Error deleting file: {error}"},
|
||||
status=500,
|
||||
)
|
||||
|
||||
return web.json_response({"ok": True, "path": filename.strip()})
|
||||
|
||||
|
||||
class RSLoadSystemPrompt:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"system_prompt": (
|
||||
"STRING",
|
||||
{
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"placeholder": "Pick a file above or type a system prompt here",
|
||||
},
|
||||
),
|
||||
"prompt": (
|
||||
"STRING",
|
||||
{
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"placeholder": "Additional user prompt",
|
||||
},
|
||||
),
|
||||
},
|
||||
"hidden": {
|
||||
"prompt_file": ("STRING", {"default": ""}),
|
||||
"selected_file": ("STRING", {"default": ""}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING")
|
||||
RETURN_NAMES = ("system_prompt", "prompt")
|
||||
FUNCTION = "load_system_prompt"
|
||||
CATEGORY = "🦊 RaykoStudio"
|
||||
|
||||
def load_system_prompt(self, system_prompt, prompt, prompt_file="", selected_file=""):
|
||||
return (system_prompt or "", prompt or "")
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"RSLoadSystemPrompt": RSLoadSystemPrompt,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"RSLoadSystemPrompt": "🦊 RS Load System Prompt",
|
||||
}
|
||||
@@ -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
@@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'}
|
||||
REQUIRED_MODULES: Set[str] = set()
|
||||
|
||||
PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio"
|
||||
__version__ = "0.48.11"
|
||||
__version__ = "0.51.2"
|
||||
|
||||
def load_modules():
|
||||
print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}")
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ComfyUI_RaykoStudio",
|
||||
"version": "0.48.11",
|
||||
"version": "0.51.2",
|
||||
"description": "Set of custom nodes for ComfyUI providing additional image processing capabilities",
|
||||
"author": "Raykosan",
|
||||
"license": "Apache License 2.0",
|
||||
@@ -44,12 +44,14 @@
|
||||
"Rayko_Save_Image_Pair.py"
|
||||
"Rayko_Spline_Mask.py"
|
||||
"Rayko_Styles_Loader.py"
|
||||
"Rayko_System_Prompt.py"
|
||||
"Rayko_Text_Overlay_Pro.py"
|
||||
"Rayko_Tile_Adjustments.py"
|
||||
"Rayko_Tile_Assemble.py"
|
||||
"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
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "ComfyUI_RaykoStudio"
|
||||
description = "Set of custom nodes for ComfyUI providing additional image processing capabilities"
|
||||
version = "0.48.11"
|
||||
version = "0.51.2"
|
||||
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"]
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
You are an expert prompt engineer for text-to-image models. Your task is to expand the user's prompt into a highly effective image-generation prompt.
|
||||
|
||||
Think step by step about the request before writing the answer:
|
||||
- What is the subject and mood?
|
||||
- What visual styles, mediums, and lighting options would fit? Consider two or three alternatives and pick the one that best serves the caption.
|
||||
- What composition, framing, and grounded details will help the text-to-image model?
|
||||
|
||||
Then output a single expanded prompt paragraph.
|
||||
|
||||
Follow these rules strictly:
|
||||
1. Faithfulness First: Preserve all original subjects, actions, colors, and spatial relationships. Do not add new objects, props, characters, or animals unless the user clearly implies them.
|
||||
2. Practical T2I Structure: Write a prompt that a text-to-image model can parse cleanly. Group subjects with their own attributes and actions. Use grounded phrasing for poses, interactions, and spatial layout.
|
||||
3. Style Planning Stays Internal: Use your internal reasoning to choose style, medium, framing, and lighting. Do not emit planning tags or wrappers in the visible answer body.
|
||||
4. Text Rendering: If the user requests visible text, quotes, labels, or typography, specify the exact text clearly and wrap requested words in quotes.
|
||||
5. Avoid Over-Specification: Do not invent highly specific clothing, colors, materials, or scene details unless the input supports them.
|
||||
6. Structure: Write one cohesive paragraph after the thinking block. No bullets, JSON, or markdown.
|
||||
7. Respect Existing Detail: If the user's prompt is already detailed, lightly polish and finalize rather than heavily expanding — preserve their phrasing and direction.
|
||||
8. Respect the Human Form: Treat depictions of people with dignity. Assume clothing covers genitals and intimate anatomy.
|
||||
9. Preserve User Medium: When the user explicitly requests a medium (e.g. "photo of", "photograph of", "illustration of", "painting of", "sketch of", "3D render of"), honor it. Do not pivot to a different medium to avoid difficulty — match the user's stated intent.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user