comfy-env 0.0.18__tar.gz → 0.0.19__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {comfy_env-0.0.18 → comfy_env-0.0.19}/PKG-INFO +1 -1
  2. {comfy_env-0.0.18 → comfy_env-0.0.19}/pyproject.toml +1 -1
  3. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/env/config.py +19 -0
  4. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/env/config_file.py +36 -3
  5. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/install.py +104 -12
  6. comfy_env-0.0.18/examples/basic_node/__init__.py +0 -5
  7. comfy_env-0.0.18/examples/basic_node/comfy-env.toml +0 -65
  8. comfy_env-0.0.18/examples/basic_node/nodes.py +0 -157
  9. comfy_env-0.0.18/examples/basic_node/worker.py +0 -79
  10. comfy_env-0.0.18/examples/decorator_node/__init__.py +0 -9
  11. comfy_env-0.0.18/examples/decorator_node/nodes.py +0 -182
  12. comfy_env-0.0.18/src/comfy_env/tools.py +0 -221
  13. {comfy_env-0.0.18 → comfy_env-0.0.19}/.github/workflows/publish.yml +0 -0
  14. {comfy_env-0.0.18 → comfy_env-0.0.19}/.gitignore +0 -0
  15. {comfy_env-0.0.18 → comfy_env-0.0.19}/LICENSE +0 -0
  16. {comfy_env-0.0.18 → comfy_env-0.0.19}/README.md +0 -0
  17. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/__init__.py +0 -0
  18. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/cli.py +0 -0
  19. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/decorator.py +0 -0
  20. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/env/__init__.py +0 -0
  21. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/env/cuda_gpu_detection.py +0 -0
  22. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/env/manager.py +0 -0
  23. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/env/platform/__init__.py +0 -0
  24. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/env/platform/base.py +0 -0
  25. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/env/platform/darwin.py +0 -0
  26. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/env/platform/linux.py +0 -0
  27. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/env/platform/windows.py +0 -0
  28. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/env/security.py +0 -0
  29. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/errors.py +0 -0
  30. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/ipc/__init__.py +0 -0
  31. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/ipc/bridge.py +0 -0
  32. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/ipc/protocol.py +0 -0
  33. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/ipc/tensor.py +0 -0
  34. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/ipc/torch_bridge.py +0 -0
  35. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/ipc/transport.py +0 -0
  36. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/ipc/worker.py +0 -0
  37. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/registry.py +0 -0
  38. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/resolver.py +0 -0
  39. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/stubs/__init__.py +0 -0
  40. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/stubs/folder_paths.py +0 -0
  41. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/wheel_sources.yml +0 -0
  42. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/workers/__init__.py +0 -0
  43. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/workers/base.py +0 -0
  44. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/workers/pool.py +0 -0
  45. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/workers/tensor_utils.py +0 -0
  46. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/workers/torch_mp.py +0 -0
  47. {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/workers/venv.py +0 -0
  48. {comfy_env-0.0.18 → comfy_env-0.0.19}/untitled.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comfy-env
3
- Version: 0.0.18
3
+ Version: 0.0.19
4
4
  Summary: Environment management for ComfyUI custom nodes - CUDA wheel resolution and process isolation
5
5
  Project-URL: Homepage, https://github.com/PozzettiAndrea/comfy-env
6
6
  Project-URL: Repository, https://github.com/PozzettiAndrea/comfy-env
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "comfy-env"
3
- version = "0.0.18"
3
+ version = "0.0.19"
4
4
  description = "Environment management for ComfyUI custom nodes - CUDA wheel resolution and process isolation"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -5,6 +5,18 @@ from pathlib import Path
5
5
  from typing import Dict, List, Optional
6
6
 
7
7
 
8
+ @dataclass
9
+ class SystemConfig:
10
+ """Configuration for system-level packages.
11
+
12
+ These are OS-level packages (apt, brew, etc.) that need to be installed
13
+ before Python packages can work properly.
14
+ """
15
+ linux: List[str] = field(default_factory=list) # apt packages
16
+ darwin: List[str] = field(default_factory=list) # brew packages (future)
17
+ windows: List[str] = field(default_factory=list) # winget packages (future)
18
+
19
+
8
20
  @dataclass
9
21
  class LocalConfig:
10
22
  """Configuration for local (host environment) installs.
@@ -37,6 +49,7 @@ class EnvManagerConfig:
37
49
  Full configuration parsed from comfy-env.toml.
38
50
 
39
51
  Supports the v2 schema:
52
+ [system] - System-level packages (apt, brew, etc.)
40
53
  [local.cuda] - CUDA packages for host environment
41
54
  [local.packages] - Regular packages for host environment
42
55
  [envname] - Isolated env definition
@@ -45,11 +58,17 @@ class EnvManagerConfig:
45
58
  [node_reqs] - Node dependencies
46
59
  [tools] - External tools (e.g., blender = "4.2")
47
60
  """
61
+ system: SystemConfig = field(default_factory=SystemConfig)
48
62
  local: LocalConfig = field(default_factory=LocalConfig)
49
63
  envs: Dict[str, "IsolatedEnv"] = field(default_factory=dict)
50
64
  node_reqs: List[NodeReq] = field(default_factory=list)
51
65
  tools: Dict[str, ToolConfig] = field(default_factory=dict)
52
66
 
67
+ @property
68
+ def has_system(self) -> bool:
69
+ """Check if there are system packages to install."""
70
+ return bool(self.system.linux or self.system.darwin or self.system.windows)
71
+
53
72
  @property
54
73
  def has_local(self) -> bool:
55
74
  """Check if there are local packages to install."""
@@ -63,7 +63,7 @@ else:
63
63
  except ImportError:
64
64
  tomllib = None # type: ignore
65
65
 
66
- from .config import IsolatedEnv, EnvManagerConfig, LocalConfig, NodeReq, ToolConfig
66
+ from .config import IsolatedEnv, EnvManagerConfig, LocalConfig, NodeReq, SystemConfig, ToolConfig
67
67
  from .cuda_gpu_detection import detect_cuda_version
68
68
 
69
69
 
@@ -332,7 +332,7 @@ def _substitute_vars(s: str, variables: Dict[str, str]) -> str:
332
332
  # =============================================================================
333
333
 
334
334
  # Reserved table names that are NOT isolated environments
335
- RESERVED_TABLES = {"local", "node_reqs", "env", "packages", "sources", "cuda", "variables", "worker", "tools"}
335
+ RESERVED_TABLES = {"local", "node_reqs", "env", "packages", "sources", "cuda", "variables", "worker", "tools", "system"}
336
336
 
337
337
 
338
338
  def load_config(
@@ -426,12 +426,14 @@ def _parse_full_config(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig
426
426
  return _convert_simple_to_full(data, base_dir)
427
427
 
428
428
  # Parse full schema
429
+ system = _parse_system_section(data.get("system", {}))
429
430
  local = _parse_local_section(data.get("local", {}))
430
431
  envs = _parse_env_sections(data, base_dir)
431
432
  node_reqs = _parse_node_reqs(data.get("node_reqs", {}))
432
433
  tools = _parse_tools_section(data.get("tools", {}))
433
434
 
434
435
  return EnvManagerConfig(
436
+ system=system,
435
437
  local=local,
436
438
  envs=envs,
437
439
  node_reqs=node_reqs,
@@ -613,6 +615,34 @@ def _parse_tools_section(tools_data: Dict[str, Any]) -> Dict[str, ToolConfig]:
613
615
  return tools
614
616
 
615
617
 
618
+ def _parse_system_section(system_data: Dict[str, Any]) -> SystemConfig:
619
+ """Parse [system] section.
620
+
621
+ Supports:
622
+ [system]
623
+ linux = ["libgl1", "libopengl0"]
624
+ darwin = ["mesa"] # future
625
+ windows = ["vcredist"] # future
626
+ """
627
+ linux = system_data.get("linux", [])
628
+ darwin = system_data.get("darwin", [])
629
+ windows = system_data.get("windows", [])
630
+
631
+ # Ensure all are lists
632
+ if not isinstance(linux, list):
633
+ linux = [linux] if linux else []
634
+ if not isinstance(darwin, list):
635
+ darwin = [darwin] if darwin else []
636
+ if not isinstance(windows, list):
637
+ windows = [windows] if windows else []
638
+
639
+ return SystemConfig(
640
+ linux=linux,
641
+ darwin=darwin,
642
+ windows=windows,
643
+ )
644
+
645
+
616
646
  def _convert_simple_to_full(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig:
617
647
  """Convert simple config format to full EnvManagerConfig.
618
648
 
@@ -622,8 +652,9 @@ def _convert_simple_to_full(data: Dict[str, Any], base_dir: Path) -> EnvManagerC
622
652
  # Parse using simple parser to get IsolatedEnv
623
653
  simple_env = _parse_config(data, base_dir)
624
654
 
625
- # Parse tools section (shared between simple and full format)
655
+ # Parse tools and system sections (shared between simple and full format)
626
656
  tools = _parse_tools_section(data.get("tools", {}))
657
+ system = _parse_system_section(data.get("system", {}))
627
658
 
628
659
  # Check if this has explicit env settings (isolated venv) vs just CUDA packages (local install)
629
660
  env_section = data.get("env", {})
@@ -632,6 +663,7 @@ def _convert_simple_to_full(data: Dict[str, Any], base_dir: Path) -> EnvManagerC
632
663
  if has_explicit_env:
633
664
  # Isolated venv config
634
665
  return EnvManagerConfig(
666
+ system=system,
635
667
  local=LocalConfig(),
636
668
  envs={simple_env.name: simple_env},
637
669
  node_reqs=[],
@@ -648,6 +680,7 @@ def _convert_simple_to_full(data: Dict[str, Any], base_dir: Path) -> EnvManagerC
648
680
  cuda_packages[req] = ""
649
681
 
650
682
  return EnvManagerConfig(
683
+ system=system,
651
684
  local=LocalConfig(
652
685
  cuda_packages=cuda_packages,
653
686
  requirements=simple_env.requirements,
@@ -24,13 +24,111 @@ import sys
24
24
  from pathlib import Path
25
25
  from typing import Any, Callable, Dict, List, Optional, Union
26
26
 
27
- from .env.config import IsolatedEnv, ToolConfig
27
+ from .env.config import IsolatedEnv, SystemConfig
28
28
  from .env.config_file import discover_env_config, load_env_from_file, load_config, discover_config
29
29
  from .env.manager import IsolatedEnvManager
30
30
  from .errors import CUDANotFoundError, DependencyError, InstallError, WheelNotFoundError
31
31
  from .registry import PACKAGE_REGISTRY, get_cuda_short2, is_registered
32
32
  from .resolver import RuntimeEnv, WheelResolver, parse_wheel_requirement
33
- from .tools import install_tool
33
+
34
+
35
+ def _install_system_packages(
36
+ system_config: SystemConfig,
37
+ log: Callable[[str], None],
38
+ dry_run: bool = False,
39
+ ) -> bool:
40
+ """
41
+ Install system-level packages (apt, brew, etc.).
42
+
43
+ Args:
44
+ system_config: SystemConfig with package lists per OS.
45
+ log: Logging callback.
46
+ dry_run: If True, show what would be installed without installing.
47
+
48
+ Returns:
49
+ True if installation succeeded or no packages needed.
50
+ """
51
+ platform = sys.platform
52
+
53
+ if platform.startswith("linux"):
54
+ packages = system_config.linux
55
+ if not packages:
56
+ return True
57
+
58
+ log(f"Installing {len(packages)} system package(s) via apt...")
59
+
60
+ if dry_run:
61
+ log(f" Would install: {', '.join(packages)}")
62
+ return True
63
+
64
+ # Check if apt-get is available
65
+ if not shutil.which("apt-get"):
66
+ log(" Warning: apt-get not found. Cannot install system packages.")
67
+ log(f" Please install manually: {', '.join(packages)}")
68
+ return True # Don't fail - just warn
69
+
70
+ # Check if we can use sudo
71
+ sudo_available = shutil.which("sudo") is not None
72
+
73
+ try:
74
+ if sudo_available:
75
+ # Try with sudo
76
+ log(" Running apt-get update...")
77
+ update_result = subprocess.run(
78
+ ["sudo", "apt-get", "update"],
79
+ capture_output=True,
80
+ text=True,
81
+ )
82
+ if update_result.returncode != 0:
83
+ log(f" Warning: apt-get update failed: {update_result.stderr.strip()}")
84
+ # Continue anyway - packages might already be cached
85
+
86
+ log(f" Installing: {', '.join(packages)}")
87
+ install_cmd = ["sudo", "apt-get", "install", "-y"] + packages
88
+ install_result = subprocess.run(
89
+ install_cmd,
90
+ capture_output=True,
91
+ text=True,
92
+ )
93
+
94
+ if install_result.returncode != 0:
95
+ log(f" Warning: apt-get install failed: {install_result.stderr.strip()}")
96
+ log(f" Please install manually: sudo apt-get install {' '.join(packages)}")
97
+ return True # Don't fail - just warn
98
+ else:
99
+ log(" System packages installed successfully.")
100
+ return True
101
+ else:
102
+ log(" Warning: sudo not available. Cannot install system packages automatically.")
103
+ log(f" Please install manually: sudo apt-get install {' '.join(packages)}")
104
+ return True # Don't fail - just warn
105
+
106
+ except Exception as e:
107
+ log(f" Warning: Failed to install system packages: {e}")
108
+ log(f" Please install manually: sudo apt-get install {' '.join(packages)}")
109
+ return True # Don't fail - just warn
110
+
111
+ elif platform == "darwin":
112
+ packages = system_config.darwin
113
+ if not packages:
114
+ return True
115
+
116
+ log(f"System packages for macOS: {', '.join(packages)}")
117
+ log(" Note: macOS system package installation not yet implemented.")
118
+ log(f" Please install manually with Homebrew: brew install {' '.join(packages)}")
119
+ return True
120
+
121
+ elif platform == "win32":
122
+ packages = system_config.windows
123
+ if not packages:
124
+ return True
125
+
126
+ log(f"System packages for Windows: {', '.join(packages)}")
127
+ log(" Note: Windows system package installation not yet implemented.")
128
+ log(f" Please install manually.")
129
+ return True
130
+
131
+ return True
34
132
 
35
133
 
36
134
  def install(
@@ -86,16 +184,10 @@ def install(
86
184
  "Create comfy-env.toml or specify path explicitly."
87
185
  )
88
186
 
89
- # Install tools first (e.g., Blender)
90
- # Tools are installed to ComfyUI root (shared across all nodes)
91
- if full_config.tools:
92
- log(f"Installing {len(full_config.tools)} tool(s)...")
93
- comfyui_root = node_dir.parent.parent # custom_nodes/../.. = ComfyUI/
94
- for name, tool_config in full_config.tools.items():
95
- if dry_run:
96
- log(f" Would install {name} {tool_config.version}")
97
- else:
98
- install_tool(tool_config, log, comfyui_root)
187
+ # Install system packages first (apt, brew, etc.)
188
+ # These need to be installed before Python packages that depend on them
189
+ if full_config.has_system:
190
+ _install_system_packages(full_config.system, log, dry_run)
99
191
 
100
192
  # Get environment config
101
193
  env_config = full_config.default_env
@@ -1,5 +0,0 @@
1
- """Example ComfyUI node using process isolation."""
2
-
3
- from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
4
-
5
- __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
@@ -1,65 +0,0 @@
1
- # Example isolation configuration for a ComfyUI custom node.
2
- #
3
- # This file defines the isolated Python environment for your node.
4
- # The comfyui-isolation library will auto-discover this file.
5
- #
6
- # Usage in your node:
7
- # bridge = WorkerBridge.from_config_file(
8
- # node_dir=Path(__file__).parent,
9
- # worker_script=Path(__file__).parent / "worker.py",
10
- # )
11
-
12
- [env]
13
- # Unique name for this environment (used for caching)
14
- name = "example-node"
15
-
16
- # Python version
17
- python = "3.10"
18
-
19
- # CUDA version: "auto" (detect), "12.4", "12.8", or null (CPU only)
20
- cuda = "auto"
21
-
22
- # Optional: specific PyTorch version (if not specified, latest from index is used)
23
- # pytorch_version = "2.4.1"
24
-
25
-
26
- [packages]
27
- # Required packages
28
- requirements = [
29
- "torch",
30
- "torchvision",
31
- "pillow",
32
- "numpy",
33
- ]
34
-
35
- # Optional: path to additional requirements.txt (relative to this file's directory)
36
- # requirements_file = "requirements.txt"
37
-
38
-
39
- [sources]
40
- # Custom wheel repositories (passed as --find-links to pip)
41
- # wheel_sources = [
42
- # "https://my-custom-wheels.github.io/",
43
- # ]
44
-
45
- # Extra package indexes (passed as --extra-index-url to pip)
46
- # index_urls = [
47
- # "https://test.pypi.org/simple/",
48
- # ]
49
-
50
-
51
- [variables]
52
- # Define variables for substitution in requirements.
53
- # You can reference these with {variable_name} syntax.
54
- #
55
- # Auto-populated variables (no need to define):
56
- # {cuda_version} - e.g., "12.4"
57
- # {cuda_short} - e.g., "124"
58
- # {pytorch_version} - e.g., "2.9.1"
59
- # {pytorch_short} - e.g., "291" (version without dots)
60
- # {pytorch_mm} - e.g., "29" (major.minor without dot, for wheel names)
61
- #
62
- # Example:
63
- # pytorch_version = "2.4.1"
64
- # pytorch3d_version = "0.7.8+5043d15pt{pytorch_version}cu{cuda_short}"
65
- # flex_gemm = "0.0.1+cu{cuda_short}torch{pytorch_mm}"
@@ -1,157 +0,0 @@
1
- """
2
- Example ComfyUI node that uses process isolation.
3
-
4
- This demonstrates how to use comfyui-isolation in a ComfyUI custom node.
5
-
6
- Two approaches are shown:
7
- 1. Programmatic configuration (IsolatedEnv directly in code)
8
- 2. Declarative configuration (comfy_env_reqs.toml file)
9
- """
10
-
11
- from pathlib import Path
12
- from comfy_env import IsolatedEnv, WorkerBridge, detect_cuda_version
13
-
14
- # Get the node's root directory
15
- NODE_ROOT = Path(__file__).parent
16
-
17
-
18
- # ===========================================================================
19
- # APPROACH 1: Programmatic Configuration (traditional)
20
- # ===========================================================================
21
- # Define the isolated environment configuration in code
22
- ENV_CONFIG = IsolatedEnv(
23
- name="example-node",
24
- python="3.10",
25
- cuda=detect_cuda_version(), # Auto-detect (12.8 for Blackwell, 12.4 for others)
26
- requirements=[
27
- "torch",
28
- "torchvision",
29
- "pillow",
30
- "numpy",
31
- ],
32
- # Add custom wheel sources if needed:
33
- # wheel_sources=["https://my-wheels.github.io/"],
34
- )
35
-
36
- # Create bridge singleton
37
- _bridge = None
38
-
39
-
40
- def get_bridge() -> WorkerBridge:
41
- """Get or create the worker bridge singleton (programmatic approach)."""
42
- global _bridge
43
- if _bridge is None:
44
- _bridge = WorkerBridge(
45
- env=ENV_CONFIG,
46
- worker_script=NODE_ROOT / "worker.py",
47
- base_dir=NODE_ROOT,
48
- )
49
- return _bridge
50
-
51
-
52
- # ===========================================================================
53
- # APPROACH 2: Declarative Configuration (recommended for new projects)
54
- # ===========================================================================
55
- # Instead of the above, you can use a comfy_env_reqs.toml file:
56
- #
57
- # def get_bridge() -> WorkerBridge:
58
- # """Get or create the worker bridge singleton (config file approach)."""
59
- # global _bridge
60
- # if _bridge is None:
61
- # _bridge = WorkerBridge.from_config_file(
62
- # node_dir=NODE_ROOT,
63
- # worker_script=NODE_ROOT / "worker.py",
64
- # )
65
- # return _bridge
66
- #
67
- # This auto-discovers comfy_env_reqs.toml in the node directory.
68
- # See the example file in this directory for the format.
69
-
70
-
71
- class IsolatedProcessorNode:
72
- """
73
- Example node that processes images in an isolated environment.
74
-
75
- This node runs inference in a separate Python process with its own
76
- dependencies, avoiding conflicts with ComfyUI's environment.
77
- """
78
-
79
- @classmethod
80
- def INPUT_TYPES(cls):
81
- return {
82
- "required": {
83
- "image": ("IMAGE",),
84
- "scale": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0}),
85
- }
86
- }
87
-
88
- RETURN_TYPES = ("IMAGE",)
89
- FUNCTION = "process"
90
- CATEGORY = "example/isolation"
91
-
92
- def process(self, image, scale):
93
- """Process image in isolated environment."""
94
- import torch
95
- import numpy as np
96
- from PIL import Image
97
-
98
- bridge = get_bridge()
99
-
100
- # Ensure environment is set up (first run only)
101
- bridge.ensure_environment(verify_packages=["torch"])
102
-
103
- # Convert ComfyUI image format to PIL
104
- # ComfyUI uses (B, H, W, C) float32 tensors
105
- img_np = (image[0].numpy() * 255).astype(np.uint8)
106
- pil_image = Image.fromarray(img_np)
107
-
108
- # Call worker method
109
- result = bridge.call("process_image", image=pil_image, scale=scale)
110
-
111
- # Convert back to ComfyUI format
112
- result_np = np.array(result).astype(np.float32) / 255.0
113
- result_tensor = torch.from_numpy(result_np).unsqueeze(0)
114
-
115
- return (result_tensor,)
116
-
117
-
118
- class SetupIsolatedEnvNode:
119
- """
120
- Node to set up the isolated environment.
121
-
122
- Run this once to create the environment before using other nodes.
123
- """
124
-
125
- @classmethod
126
- def INPUT_TYPES(cls):
127
- return {
128
- "required": {
129
- "trigger": ("*",), # Any input to trigger
130
- }
131
- }
132
-
133
- RETURN_TYPES = ("STRING",)
134
- FUNCTION = "setup"
135
- CATEGORY = "example/isolation"
136
-
137
- def setup(self, trigger):
138
- """Set up the isolated environment."""
139
- bridge = get_bridge()
140
-
141
- try:
142
- bridge.ensure_environment(verify_packages=["torch", "numpy"])
143
- return (f"Environment ready: {bridge.python_exe}",)
144
- except Exception as e:
145
- return (f"Setup failed: {e}",)
146
-
147
-
148
- # Node class mappings for ComfyUI
149
- NODE_CLASS_MAPPINGS = {
150
- "IsolatedProcessorExample": IsolatedProcessorNode,
151
- "SetupIsolatedEnvExample": SetupIsolatedEnvNode,
152
- }
153
-
154
- NODE_DISPLAY_NAME_MAPPINGS = {
155
- "IsolatedProcessorExample": "Isolated Processor (Example)",
156
- "SetupIsolatedEnvExample": "Setup Isolated Env (Example)",
157
- }
@@ -1,79 +0,0 @@
1
- """
2
- Example worker script for isolated environment.
3
-
4
- This runs in the isolated Python environment and handles inference requests.
5
- """
6
-
7
- from comfy_env import BaseWorker, register
8
-
9
-
10
- class ExampleWorker(BaseWorker):
11
- """Example worker that processes images."""
12
-
13
- def setup(self):
14
- """Called once when worker starts - load models here."""
15
- self.log("Loading model...")
16
-
17
- # Import heavy dependencies only in the isolated env
18
- import torch
19
- self.device = "cuda" if torch.cuda.is_available() else "cpu"
20
- self.log(f"Using device: {self.device}")
21
-
22
- # In a real node, you'd load your model here:
23
- # self.model = load_my_model().to(self.device)
24
-
25
- self.log("Model loaded!")
26
-
27
- @register("echo")
28
- def echo(self, message: str):
29
- """Simple echo for testing."""
30
- return f"Echo: {message}"
31
-
32
- @register("process_image")
33
- def process_image(self, image, scale: float = 1.0):
34
- """
35
- Process an image.
36
-
37
- Args:
38
- image: PIL Image or numpy array
39
- scale: Scale factor
40
-
41
- Returns:
42
- Processed image
43
- """
44
- import numpy as np
45
- from PIL import Image
46
-
47
- self.log(f"Processing image with scale={scale}")
48
-
49
- # Convert to numpy if needed
50
- if hasattr(image, 'save'): # PIL Image
51
- arr = np.array(image)
52
- else:
53
- arr = image
54
-
55
- # Example processing: simple brightness adjustment
56
- result = np.clip(arr * scale, 0, 255).astype(np.uint8)
57
-
58
- self.log("Processing complete")
59
- return Image.fromarray(result)
60
-
61
- @register("gpu_info")
62
- def get_gpu_info(self):
63
- """Get GPU information from the isolated environment."""
64
- import torch
65
-
66
- if not torch.cuda.is_available():
67
- return {"available": False}
68
-
69
- return {
70
- "available": True,
71
- "device_count": torch.cuda.device_count(),
72
- "device_name": torch.cuda.get_device_name(0),
73
- "memory_allocated": torch.cuda.memory_allocated(0),
74
- "memory_cached": torch.cuda.memory_reserved(0),
75
- }
76
-
77
-
78
- if __name__ == "__main__":
79
- ExampleWorker().run()
@@ -1,9 +0,0 @@
1
- """
2
- Example ComfyUI nodes using the @isolated decorator.
3
-
4
- This demonstrates the simplest way to run node methods in isolated subprocess.
5
- """
6
-
7
- from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
8
-
9
- __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
@@ -1,182 +0,0 @@
1
- """
2
- Example ComfyUI nodes using the @isolated decorator.
3
-
4
- This demonstrates the simplest possible way to run node methods in
5
- isolated subprocess environments. No separate worker file needed!
6
-
7
- The @isolated decorator:
8
- 1. Extracts your method source code at class decoration time
9
- 2. Auto-generates a worker file on first call
10
- 3. Creates an isolated venv with your requirements
11
- 4. Intercepts method calls and forwards to the subprocess
12
- 5. Handles all serialization/deserialization automatically
13
-
14
- Compare this to examples/basic_node which uses the traditional approach
15
- with separate worker.py and explicit bridge management.
16
- """
17
-
18
- from comfy_env import isolated
19
-
20
-
21
- # ===========================================================================
22
- # EXAMPLE 1: Simple inline requirements
23
- # ===========================================================================
24
-
25
- @isolated(
26
- env="decorator-example",
27
- requirements=["torch", "pillow", "numpy"],
28
- python="3.10",
29
- cuda="auto", # Auto-detect GPU (12.8 for Blackwell, 12.4 for others)
30
- )
31
- class SimpleIsolatedNode:
32
- """
33
- Simplest possible isolated node - just add @isolated decorator!
34
-
35
- The process() method body runs in a separate Python subprocess
36
- with its own torch installation. The main ComfyUI process never
37
- imports torch from this node.
38
- """
39
-
40
- @classmethod
41
- def INPUT_TYPES(cls):
42
- return {
43
- "required": {
44
- "image": ("IMAGE",),
45
- "brightness": ("FLOAT", {
46
- "default": 1.0,
47
- "min": 0.0,
48
- "max": 2.0,
49
- "step": 0.1,
50
- }),
51
- }
52
- }
53
-
54
- RETURN_TYPES = ("IMAGE",)
55
- FUNCTION = "process"
56
- CATEGORY = "example/decorator"
57
-
58
- def process(self, image, brightness):
59
- # Everything below runs in isolated subprocess!
60
- # These imports happen in the subprocess, not ComfyUI main process
61
- import torch
62
- import numpy as np
63
-
64
- # The image arrives already deserialized as a torch tensor
65
- # (comfyui-isolation handles ComfyUI IMAGE format automatically)
66
-
67
- # Apply brightness adjustment
68
- result = image * brightness
69
- result = torch.clamp(result, 0.0, 1.0)
70
-
71
- return (result,)
72
-
73
-
74
- # ===========================================================================
75
- # EXAMPLE 2: Multiple isolated methods
76
- # ===========================================================================
77
-
78
- @isolated(
79
- env="multi-method-example",
80
- requirements=["torch", "pillow", "numpy"],
81
- )
82
- class MultiMethodNode:
83
- """
84
- Node with multiple methods that all run in the isolated subprocess.
85
-
86
- Use ISOLATED_METHODS to specify which methods should be isolated.
87
- """
88
-
89
- @classmethod
90
- def INPUT_TYPES(cls):
91
- return {
92
- "required": {
93
- "image": ("IMAGE",),
94
- "operation": (["invert", "grayscale", "double"],),
95
- }
96
- }
97
-
98
- RETURN_TYPES = ("IMAGE",)
99
- FUNCTION = "process"
100
- CATEGORY = "example/decorator"
101
-
102
- # List all methods that should run in subprocess
103
- ISOLATED_METHODS = ["process", "invert_image", "to_grayscale", "double_brightness"]
104
-
105
- def process(self, image, operation):
106
- import torch
107
-
108
- if operation == "invert":
109
- return self.invert_image(image)
110
- elif operation == "grayscale":
111
- return self.to_grayscale(image)
112
- else:
113
- return self.double_brightness(image)
114
-
115
- def invert_image(self, image):
116
- import torch
117
- return (1.0 - image,)
118
-
119
- def to_grayscale(self, image):
120
- import torch
121
- # Weighted grayscale conversion
122
- weights = torch.tensor([0.299, 0.587, 0.114])
123
- gray = (image[..., :3] * weights).sum(dim=-1, keepdim=True)
124
- gray = gray.expand_as(image[..., :3])
125
- if image.shape[-1] == 4: # Has alpha
126
- gray = torch.cat([gray, image[..., 3:4]], dim=-1)
127
- return (gray,)
128
-
129
- def double_brightness(self, image):
130
- import torch
131
- return (torch.clamp(image * 2.0, 0.0, 1.0),)
132
-
133
-
134
- # ===========================================================================
135
- # EXAMPLE 3: Using TOML config for complex dependencies
136
- # ===========================================================================
137
-
138
- # For complex dependencies with custom wheel sources, use a config file:
139
- #
140
- # @isolated(env="complex-deps", config="isolation_config.toml")
141
- # class ComplexDepsNode:
142
- # """Node with complex dependencies defined in TOML."""
143
- #
144
- # FUNCTION = "process"
145
- #
146
- # def process(self, image):
147
- # import pytorch3d # From custom wheel index
148
- # import nvdiffrast # From custom wheel index
149
- # ...
150
- #
151
- # The isolation_config.toml would look like:
152
- #
153
- # [env]
154
- # name = "complex-deps"
155
- # python = "3.10"
156
- # cuda = "auto"
157
- #
158
- # [packages]
159
- # requirements = [
160
- # "torch=={pytorch_version}",
161
- # "pytorch3d==0.7.8+5043d15pt{pytorch_version}cu{cuda_short}",
162
- # ]
163
- #
164
- # [sources]
165
- # index_urls = [
166
- # "https://pozzettiandrea.github.io/sam3dobjects-wheels/",
167
- # ]
168
-
169
-
170
- # ===========================================================================
171
- # Node registration
172
- # ===========================================================================
173
-
174
- NODE_CLASS_MAPPINGS = {
175
- "SimpleIsolatedExample": SimpleIsolatedNode,
176
- "MultiMethodIsolatedExample": MultiMethodNode,
177
- }
178
-
179
- NODE_DISPLAY_NAME_MAPPINGS = {
180
- "SimpleIsolatedExample": "Simple Isolated (Decorator Example)",
181
- "MultiMethodIsolatedExample": "Multi-Method Isolated (Decorator Example)",
182
- }
@@ -1,221 +0,0 @@
1
- """
2
- Tool installers for external dependencies like Blender.
3
-
4
- Usage in comfy-env.toml:
5
- [tools]
6
- blender = "4.2"
7
- """
8
-
9
- import os
10
- import platform
11
- import shutil
12
- import subprocess
13
- import tarfile
14
- import zipfile
15
- from pathlib import Path
16
- from typing import Callable, Optional
17
- from urllib.request import urlretrieve
18
-
19
- from .env.config import ToolConfig
20
-
21
- # Default install location
22
- DEFAULT_TOOLS_DIR = Path.home() / ".comfy-env" / "tools"
23
-
24
- # Blender download URLs by platform and version
25
- BLENDER_DOWNLOADS = {
26
- "4.2": {
27
- "linux": "https://download.blender.org/release/Blender4.2/blender-4.2.0-linux-x64.tar.xz",
28
- "windows": "https://download.blender.org/release/Blender4.2/blender-4.2.0-windows-x64.zip",
29
- "darwin": "https://download.blender.org/release/Blender4.2/blender-4.2.0-macos-arm64.dmg",
30
- },
31
- "4.3": {
32
- "linux": "https://download.blender.org/release/Blender4.3/blender-4.3.0-linux-x64.tar.xz",
33
- "windows": "https://download.blender.org/release/Blender4.3/blender-4.3.0-windows-x64.zip",
34
- "darwin": "https://download.blender.org/release/Blender4.3/blender-4.3.0-macos-arm64.dmg",
35
- },
36
- }
37
-
38
-
39
- def get_platform() -> str:
40
- """Get current platform name."""
41
- system = platform.system().lower()
42
- if system == "linux":
43
- return "linux"
44
- elif system == "windows":
45
- return "windows"
46
- elif system == "darwin":
47
- return "darwin"
48
- return system
49
-
50
-
51
- def install_tool(
52
- config: ToolConfig,
53
- log: Callable[[str], None] = print,
54
- base_dir: Optional[Path] = None,
55
- ) -> Optional[Path]:
56
- """Install a tool based on its config.
57
-
58
- Args:
59
- config: Tool configuration
60
- log: Logging callback
61
- base_dir: Base directory for tools. Tools install to base_dir/tools/<name>/
62
- """
63
- if config.name.lower() == "blender":
64
- return install_blender(config.version, log, config.install_dir or base_dir)
65
- else:
66
- log(f"Unknown tool: {config.name}")
67
- return None
68
-
69
-
70
- def install_blender(
71
- version: str = "4.2",
72
- log: Callable[[str], None] = print,
73
- base_dir: Optional[Path] = None,
74
- ) -> Optional[Path]:
75
- """
76
- Install Blender to the specified directory.
77
-
78
- Args:
79
- version: Blender version to install (e.g., "4.2")
80
- log: Logging callback
81
- base_dir: Base directory. Blender installs to base_dir/tools/blender/
82
- If None, uses ~/.comfy-env/tools/blender/
83
-
84
- Returns path to blender executable if successful.
85
- """
86
- plat = get_platform()
87
- if base_dir:
88
- install_dir = base_dir / "tools" / "blender"
89
- else:
90
- install_dir = DEFAULT_TOOLS_DIR / "blender"
91
-
92
- # Check if already installed
93
- exe = find_blender(install_dir)
94
- if exe:
95
- log(f"Blender already installed: {exe}")
96
- return exe
97
-
98
- # Get download URL
99
- if version not in BLENDER_DOWNLOADS:
100
- log(f"Unknown Blender version: {version}. Available: {list(BLENDER_DOWNLOADS.keys())}")
101
- return None
102
-
103
- urls = BLENDER_DOWNLOADS[version]
104
- if plat not in urls:
105
- log(f"Blender {version} not available for {plat}")
106
- return None
107
-
108
- url = urls[plat]
109
- log(f"Downloading Blender {version} for {plat}...")
110
-
111
- install_dir.mkdir(parents=True, exist_ok=True)
112
- archive_name = url.split("/")[-1]
113
- archive_path = install_dir / archive_name
114
-
115
- try:
116
- # Download
117
- urlretrieve(url, archive_path)
118
- log(f"Downloaded to {archive_path}")
119
-
120
- # Extract
121
- log("Extracting...")
122
- if archive_name.endswith(".tar.xz"):
123
- with tarfile.open(archive_path, "r:xz") as tar:
124
- tar.extractall(install_dir)
125
- elif archive_name.endswith(".zip"):
126
- with zipfile.ZipFile(archive_path, "r") as zf:
127
- zf.extractall(install_dir)
128
- elif archive_name.endswith(".dmg"):
129
- # macOS DMG requires special handling
130
- log("macOS DMG installation not yet automated. Please install manually.")
131
- archive_path.unlink()
132
- return None
133
-
134
- # Clean up archive
135
- archive_path.unlink()
136
-
137
- # Find the executable
138
- exe = find_blender(install_dir)
139
- if exe:
140
- log(f"Blender installed: {exe}")
141
- return exe
142
- else:
143
- log("Blender extracted but executable not found")
144
- return None
145
-
146
- except Exception as e:
147
- log(f"Failed to install Blender: {e}")
148
- if archive_path.exists():
149
- archive_path.unlink()
150
- return None
151
-
152
-
153
- def find_blender(search_dir: Optional[Path] = None) -> Optional[Path]:
154
- """Find Blender executable."""
155
- # Check PATH first
156
- blender_in_path = shutil.which("blender")
157
- if blender_in_path:
158
- return Path(blender_in_path)
159
-
160
- # Check common locations
161
- plat = get_platform()
162
- search_paths = []
163
-
164
- if search_dir:
165
- search_paths.append(search_dir)
166
-
167
- search_paths.append(DEFAULT_TOOLS_DIR / "blender")
168
-
169
- if plat == "windows":
170
- search_paths.extend([
171
- Path(os.environ.get("ProgramFiles", "C:/Program Files")) / "Blender Foundation",
172
- Path.home() / "AppData" / "Local" / "Blender Foundation",
173
- ])
174
- elif plat == "linux":
175
- search_paths.extend([
176
- Path("/opt/blender"),
177
- Path.home() / "blender",
178
- ])
179
- elif plat == "darwin":
180
- search_paths.append(Path("/Applications/Blender.app/Contents/MacOS"))
181
-
182
- exe_name = "blender.exe" if plat == "windows" else "blender"
183
-
184
- for base in search_paths:
185
- if not base.exists():
186
- continue
187
- # Direct check
188
- exe = base / exe_name
189
- if exe.exists():
190
- return exe
191
- # Search subdirectories (for extracted archives like blender-4.2.0-linux-x64/)
192
- for subdir in base.iterdir():
193
- if subdir.is_dir():
194
- exe = subdir / exe_name
195
- if exe.exists():
196
- return exe
197
-
198
- return None
199
-
200
-
201
- def ensure_blender(
202
- version: str = "4.2",
203
- log: Callable[[str], None] = print,
204
- base_dir: Optional[Path] = None,
205
- ) -> Optional[Path]:
206
- """Ensure Blender is installed, installing if necessary.
207
-
208
- Args:
209
- version: Blender version to install
210
- log: Logging callback
211
- base_dir: Base directory. Searches/installs in base_dir/tools/blender/
212
- """
213
- if base_dir:
214
- search_dir = base_dir / "tools" / "blender"
215
- else:
216
- search_dir = DEFAULT_TOOLS_DIR / "blender"
217
-
218
- exe = find_blender(search_dir)
219
- if exe:
220
- return exe
221
- return install_blender(version, log, base_dir)
File without changes
File without changes
File without changes
File without changes