Merge branch 'pr/10' into 'main'

Add Nodes for Opening and Closing Projects

See merge request lightspeedrtx/lss-ai-tools/comfyui-rtx_remix!28
This commit is contained in:
Pierre-Olivier Trottier
2025-07-25 10:57:13 -07:00
8 changed files with 10400 additions and 294 deletions
+18 -1
View File
@@ -10,12 +10,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
### Changed
- Update nodes to be compatible with the renamed models
### Fixed
### Removed
## [1.1.0] - 2025-07-24
### Added
- **Open Project Node**: Opens RTX Remix projects by layer ID for workflow management
- **Get Loaded Project Node**: Gets layer ID of current open project for use with Open Project node
- **Close Project Node**: Closes RTX Remix projects and a force boolean parameter default false
- **Get Default Directory Node**: Captures the RTX Remix default output directory before closing projects
- Low VRAM workflow example: "rtx_remix_pbrify_workflow_LowVRAM.json" demonstrating new project management nodes
### Changed
- **Ingest Texture Node**: Made output_folder parameter mandatory, must be provided by user or Get Default Directory Node
- Updated normal PBRify workflow to work with new ingest texture changes
### Fixed
- Path handling compatibility for URLs with spaces when calling toolkit
### Removed
## [2024.0.0]
### Added
+5 -1
View File
@@ -19,7 +19,8 @@ You should see RTX Remix nodes:
- **RTX Remix Texture Types**: Select multiple texture types from a list of supported texture types.
### Ingestion
- **RTX Remix Ingest Texture**: Ingest an image as a texture and save it to disk
- **RTX Remix Ingest Texture**: Ingest an image as a texture and save it to disk. Output folder is now mandatory and must be provided by user or Get Default Directory node
- **RTX Remix Get Default Directory**: Captures the RTX Remix default output directory before closing projects for use with texture ingestion
### Common
- **RTX Remix Rest API Details**: Provide the port information to connect to the RTX Remix Toolkit
@@ -38,6 +39,9 @@ You should see RTX Remix nodes:
- **RTX Remix Get Edit Target**: Get the edit target from the currently open project
- **RTX Remix Layer Types**: Select multiple layer types from a list of supported layer types.
- **RTX Remix Layer Type**: Select from a list of supported layer types.
- **RTX Remix Open Project**: Opens RTX Remix projects by layer ID for workflow management and VRAM optimization
- **RTX Remix Get Loaded Project**: Gets layer ID of current open project for use with Open Project node
- **RTX Remix Close Project**: Closes RTX Remix projects to save VRAM during AI processing
## Example Workflows and Templates
+12 -1
View File
@@ -26,15 +26,18 @@ from .common import (
Switch,
)
from .file import DeleteFile
from .ingestion import IngestTexture
from .ingestion import GetDefaultDirectory, IngestTexture
from .layers import (
CloseProject,
CreateLayer,
DefineLayerId,
GetEditTarget,
GetLayers,
GetLoadedProject,
LayerType,
LayerTypes,
MuteLayer,
OpenProject,
RemoveLayer,
SaveLayer,
SetEditTarget,
@@ -50,18 +53,22 @@ from .textures import (
# A dictionary that contains all nodes you want to export with their names
# NOTE: names should be globally unique
NODE_CLASS_MAPPINGS = {
"RTXRemixCloseProject": CloseProject,
"RTXRemixCreateLayer": CreateLayer,
"RTXRemixDefineLayerId": DefineLayerId,
"RTXRemixDeleteFile": DeleteFile,
"RTXRemixEndContext": EndContext,
"RTXRemixGetDefaultDirectory": GetDefaultDirectory,
"RTXRemixGetEditTarget": GetEditTarget,
"RTXRemixGetLayers": GetLayers,
"RTXRemixGetLoadedProject": GetLoadedProject,
"RTXRemixGetTextures": GetTextures,
"RTXRemixIngestTexture": IngestTexture,
"RTXRemixInvertBool": InvertBool,
"RTXRemixLayerType": LayerType,
"RTXRemixLayerTypes": LayerTypes,
"RTXRemixMuteLayer": MuteLayer,
"RTXRemixOpenProject": OpenProject,
"RTXRemixRemoveLayer": RemoveLayer,
"RTXRemixRestAPIDetails": RestAPIDetails,
"RTXRemixSaveLayer": SaveLayer,
@@ -79,18 +86,22 @@ NODE_CLASS_MAPPINGS = {
# A dictionary that contains the friendly/humanly readable titles for the nodes
NODE_DISPLAY_NAME_MAPPINGS = {
"RTXRemixCloseProject": "RTX Remix Close Project",
"RTXRemixCreateLayer": "RTX Remix Create Layer",
"RTXRemixDefineLayerId": "RTX Remix Define Layer ID",
"RTXRemixDeleteFile": "RTX Remix Delete File",
"RTXRemixEndContext": "RTX Remix End Context",
"RTXRemixGetDefaultDirectory": "RTX Remix Get Default Directory",
"RTXRemixGetEditTarget": "RTX Remix Get Edit Target",
"RTXRemixGetLayers": "RTX Remix Get Layers",
"RTXRemixGetLoadedProject": "RTX Remix Get Loaded Project",
"RTXRemixGetTextures": "RTX Remix Get Textures",
"RTXRemixIngestTexture": "RTX Remix Ingest Texture",
"RTXRemixInvertBool": "RTX Remix Invert Boolean Value",
"RTXRemixLayerType": "RTX Remix Layer Type",
"RTXRemixLayerTypes": "RTX Remix Layer Types",
"RTXRemixMuteLayer": "RTX Remix Mute Layer",
"RTXRemixOpenProject": "RTX Remix Open Project",
"RTXRemixRemoveLayer": "RTX Remix Remove Layer",
"RTXRemixRestAPIDetails": "RTX Remix Rest API Details",
"RTXRemixSaveLayer": "RTX Remix Save Layer",
+44 -26
View File
@@ -27,7 +27,7 @@ from PIL import Image
from .common import add_context_input_enabled_and_output
from .constant import HEADER_LSS_REMIX_VERSION_1_0, PREFIX_MENU
from .utils import check_response_status_code
from .utils import check_response_status_code, posix
_file_name = pathlib.Path(__file__).stem
@@ -57,21 +57,12 @@ class IngestTexture:
"forceInput": True,
},
),
},
"optional": {
"enable_override_output_folder": (
"BOOLEAN",
{
"default": False,
"label_on": "enabled",
"label_off": "disabled",
},
),
"override_output_folder": (
"output_directory": (
"STRING",
{
# node
"default": "",
"forceInput": True,
},
),
},
@@ -91,24 +82,14 @@ class IngestTexture:
texture: torch.Tensor,
texture_type: str,
texture_name: str,
enable_override_output_folder: bool,
override_output_folder: str,
output_directory: str,
):
if not self.enable_this_node: # noqa
return ("",)
address, port = self.context # noqa
if enable_override_output_folder:
if not pathlib.Path(override_output_folder).exists():
raise FileNotFoundError("Can't overwrite output folder, folder doesn't exist.")
output_folder = override_output_folder
else:
# call RestAPI to get the default output folder
r = requests.get(
f"http://{address}:{port}/stagecraft/assets/default-directory",
headers=HEADER_LSS_REMIX_VERSION_1_0,
)
check_response_status_code(r)
output_folder = json.loads(r.text).get("directory_path", {})
if not pathlib.Path(output_directory).exists():
raise FileNotFoundError(f"Output directory doesn't exist: {output_directory}")
output_folder = output_directory
full_output_folder, filename, _counter, _subfolder, _filename_prefix = folder_paths.get_save_image_path(
texture_name, folder_paths.get_output_directory(), texture[0].shape[1], texture[0].shape[0]
@@ -161,3 +142,40 @@ class IngestTexture:
raise FileNotFoundError(f"Can't find the texture {result_path}")
return (str(result_path),)
@add_context_input_enabled_and_output
class GetDefaultDirectory:
"""Get the default directory from RTX Remix before closing project"""
@classmethod
def INPUT_TYPES(cls): # noqa N802
inputs = {
"required": {},
}
return inputs
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("default_directory",)
FUNCTION = "get_default_directory"
OUTPUT_NODE = False
CATEGORY = f"{PREFIX_MENU}/{_file_name}"
def _get_default_output_directory(self) -> str:
"""Utility method to get default output directory from RTX Remix API."""
address, port = self.context # noqa
r = requests.get(
f"http://{address}:{port}/stagecraft/assets/default-directory",
headers=HEADER_LSS_REMIX_VERSION_1_0,
)
check_response_status_code(r)
return json.loads(r.text).get("directory_path", "")
def get_default_directory(self):
if not self.enable_this_node: # noqa
return ("",)
default_directory = self._get_default_output_directory()
return (default_directory,)
+96 -6
View File
@@ -20,7 +20,7 @@ import collections
import json
import pathlib
import re
from urllib.parse import quote_plus, unquote
from urllib.parse import quote, unquote
import requests
@@ -329,7 +329,7 @@ class GetLayers:
address, port = self.context # noqa
if parent_layer_id:
r = requests.get(
f"http://{address}:{port}/stagecraft/layers/{quote_plus(posix(parent_layer_id))}/sublayers",
f"http://{address}:{port}/stagecraft/layers/{quote(posix(parent_layer_id), safe='')}/sublayers",
params=params,
headers=HEADER_LSS_REMIX_VERSION_1_0,
)
@@ -433,7 +433,7 @@ class MuteLayer(_LayerOp):
payload = {"value": mute}
address, port = self.context # noqa
r = requests.put(
f"http://{address}:{port}/stagecraft/layers/{quote_plus(posix(layer_id))}/mute",
f"http://{address}:{port}/stagecraft/layers/{quote(posix(layer_id), safe='')}/mute",
data=json.dumps(payload),
headers=HEADER_LSS_REMIX_VERSION_1_0,
)
@@ -464,7 +464,7 @@ class RemoveLayer(_LayerOp):
payload = {"parent_layer_id": posix(parent_layer_id)}
address, port = self.context # noqa
r = requests.delete(
f"http://{address}:{port}/stagecraft/layers/{quote_plus(posix(layer_id))}",
f"http://{address}:{port}/stagecraft/layers/{quote(posix(layer_id), safe='')}",
data=json.dumps(payload),
headers=HEADER_LSS_REMIX_VERSION_1_0,
)
@@ -481,7 +481,7 @@ class SaveLayer(_LayerOp):
return ("",)
address, port = self.context # noqa
r = requests.post(
f"http://{address}:{port}/stagecraft/layers/{quote_plus(posix(layer_id))}/save",
f"http://{address}:{port}/stagecraft/layers/{quote(posix(layer_id), safe='')}/save",
headers=HEADER_LSS_REMIX_VERSION_1_0,
)
check_response_status_code(r)
@@ -531,8 +531,98 @@ class SetEditTarget(_LayerOp):
return ("",)
address, port = self.context # noqa
r = requests.put(
f"http://{address}:{port}/stagecraft/layers/target/{quote_plus(posix(layer_id))}",
f"http://{address}:{port}/stagecraft/layers/target/{quote(posix(layer_id), safe='')}",
headers=HEADER_LSS_REMIX_VERSION_1_0,
)
check_response_status_code(r)
return (layer_id,) # return an output so that you can make sure this executes before other nodes
@add_context_input_enabled_and_output
class CloseProject:
"""Close the currently open project"""
@classmethod
def INPUT_TYPES(cls): # noqa N802
inputs = get_context_inputs()
inputs["optional"] = {
"force": ("BOOLEAN", {"default": False}),
}
return inputs
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("status",)
FUNCTION = "close_project"
OUTPUT_NODE = False
CATEGORY = f"{PREFIX_MENU}/{_file_name}"
def close_project(self, force: bool = False) -> tuple[str]:
if not self.enable_this_node: # noqa
return ("disabled",)
address, port = self.context # noqa
url = f"http://{address}:{port}/stagecraft/project"
if force:
url += "?force=true"
r = requests.delete(
url,
headers=HEADER_LSS_REMIX_VERSION_1_0,
)
check_response_status_code(r)
return ("closed",)
@add_context_input_enabled_and_output
class OpenProject(_LayerOp):
"""Open a project using the specified layer ID"""
def execute(self, layer_id: str) -> tuple[str]:
if not self.enable_this_node: # noqa
return ("",)
address, port = self.context # noqa
r = requests.put(
f"http://{address}:{port}/stagecraft/project/{quote(posix(layer_id), safe='')}",
headers=HEADER_LSS_REMIX_VERSION_1_0,
)
check_response_status_code(r)
return (layer_id,)
@add_context_input_enabled_and_output
class GetLoadedProject:
"""Get the currently loaded project"""
@classmethod
def INPUT_TYPES(cls): # noqa N802
return get_context_inputs()
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("layer_id",)
FUNCTION = "get_loaded_project"
OUTPUT_NODE = False
CATEGORY = f"{PREFIX_MENU}/{_file_name}"
def get_loaded_project(self) -> tuple[str]:
if not self.enable_this_node: # noqa
return ("",)
address, port = self.context # noqa
r = requests.get(
f"http://{address}:{port}/stagecraft/project/",
headers=HEADER_LSS_REMIX_VERSION_1_0,
)
check_response_status_code(r)
response_data = json.loads(r.text)
layer_id = response_data.get("layer_id", "")
return (layer_id,)
@classmethod
def IS_CHANGED(cls, **kwargs): # noqa N802
"""
Always process the node in case the loaded project in the RTX Remix app changed
"""
return float("nan")
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-rtx-remix"
description = "Use ComfyUI with RTX Remix to remaster classic games [a/https://github.com/NVIDIAGameWorks/rtx-remix](https://github.com/NVIDIAGameWorks/rtx-remix)"
version = "1.0.1"
version = "1.1.0"
license = {file = "LICENSE"}
dependencies = ["numpy", "pillow>=10.0.1", "requests", "torch"]
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff