pynode-flow 0.2.0__py3-none-any.whl

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 (227) hide show
  1. pynode/__init__.py +37 -0
  2. pynode/__main__.py +9 -0
  3. pynode/_perf.py +65 -0
  4. pynode/_version.py +24 -0
  5. pynode/api/__init__.py +15 -0
  6. pynode/api/helpers.py +79 -0
  7. pynode/api/nodes.py +423 -0
  8. pynode/api/services.py +124 -0
  9. pynode/api/sse.py +58 -0
  10. pynode/api/uploads.py +59 -0
  11. pynode/api/workflows.py +466 -0
  12. pynode/config.py +93 -0
  13. pynode/install_nodes.py +143 -0
  14. pynode/main.py +118 -0
  15. pynode/node_registry.py +156 -0
  16. pynode/nodes/AutoSyncNode/auto_sync_node.py +151 -0
  17. pynode/nodes/BBoxMetricsNode/__init__.py +3 -0
  18. pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +250 -0
  19. pynode/nodes/BBoxMetricsNode/requirements.txt +1 -0
  20. pynode/nodes/BatchNode/__init__.py +3 -0
  21. pynode/nodes/BatchNode/batch_node.py +86 -0
  22. pynode/nodes/BatchNode/requirements.txt +1 -0
  23. pynode/nodes/CameraNode/__init__.py +4 -0
  24. pynode/nodes/CameraNode/camera_node.py +204 -0
  25. pynode/nodes/CameraNode/requirements.txt +2 -0
  26. pynode/nodes/ChangeNode/__init__.py +1 -0
  27. pynode/nodes/ChangeNode/change_node.py +255 -0
  28. pynode/nodes/ChangeNode/requirements.txt +1 -0
  29. pynode/nodes/ConfidenceFilterNode/__init__.py +4 -0
  30. pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +208 -0
  31. pynode/nodes/ConfidenceFilterNode/requirements.txt +1 -0
  32. pynode/nodes/CounterNode/__init__.py +3 -0
  33. pynode/nodes/CounterNode/counter_node.py +191 -0
  34. pynode/nodes/CropNode/__init__.py +4 -0
  35. pynode/nodes/CropNode/crop_node.py +317 -0
  36. pynode/nodes/CropNode/requirements.txt +2 -0
  37. pynode/nodes/DebugNode/__init__.py +4 -0
  38. pynode/nodes/DebugNode/debug_node.py +169 -0
  39. pynode/nodes/DebugNode/requirements.txt +1 -0
  40. pynode/nodes/DelayNode/delay_node.py +218 -0
  41. pynode/nodes/DelayNode/requirements.txt +1 -0
  42. pynode/nodes/DrawPredictionsNode/__init__.py +3 -0
  43. pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +365 -0
  44. pynode/nodes/DrawPredictionsNode/requirements.txt +2 -0
  45. pynode/nodes/ErrorNode/error_node.py +94 -0
  46. pynode/nodes/ErrorNode/requirements.txt +1 -0
  47. pynode/nodes/FilterNode/__init__.py +3 -0
  48. pynode/nodes/FilterNode/filter_node.py +102 -0
  49. pynode/nodes/FilterNode/requirements.txt +1 -0
  50. pynode/nodes/FrameSourceNode/__init__.py +4 -0
  51. pynode/nodes/FrameSourceNode/frame_source_node.py +319 -0
  52. pynode/nodes/FrameSourceNode/requirements.txt +1 -0
  53. pynode/nodes/FunctionNode/__init__.py +3 -0
  54. pynode/nodes/FunctionNode/function_node.py +168 -0
  55. pynode/nodes/FunctionNode/requirements.txt +1 -0
  56. pynode/nodes/GateNode/gate_node.py +60 -0
  57. pynode/nodes/GateNode/requirements.txt +1 -0
  58. pynode/nodes/GetiUploadNode/__init__.py +3 -0
  59. pynode/nodes/GetiUploadNode/geti_upload_node.py +278 -0
  60. pynode/nodes/GetiUploadNode/requirements.txt +1 -0
  61. pynode/nodes/ImageFormatNode/__init__.py +4 -0
  62. pynode/nodes/ImageFormatNode/image_format_node.py +310 -0
  63. pynode/nodes/ImageFormatNode/requirements.txt +2 -0
  64. pynode/nodes/ImageUploadNode/image_upload_node.py +96 -0
  65. pynode/nodes/ImageViewerNode/__init__.py +4 -0
  66. pynode/nodes/ImageViewerNode/image_viewer_node.py +162 -0
  67. pynode/nodes/ImageViewerNode/requirements.txt +2 -0
  68. pynode/nodes/ImageWriterNode/__init__.py +3 -0
  69. pynode/nodes/ImageWriterNode/imagewriter_node.py +352 -0
  70. pynode/nodes/ImageWriterNode/requirements.txt +1 -0
  71. pynode/nodes/InferenceNode/InferenceEngine/__init__.py +15 -0
  72. pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +149 -0
  73. pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +155 -0
  74. pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +272 -0
  75. pynode/nodes/InferenceNode/InferenceEngine/engines/geti_engine.py +309 -0
  76. pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +346 -0
  77. pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +86 -0
  78. pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +863 -0
  79. pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +533 -0
  80. pynode/nodes/InferenceNode/InferenceEngine/readme.md +107 -0
  81. pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +121 -0
  82. pynode/nodes/InferenceNode/inference_node.py +451 -0
  83. pynode/nodes/InferenceNode/requirements.txt +13 -0
  84. pynode/nodes/InjectNode/inject_node.py +245 -0
  85. pynode/nodes/InjectNode/requirements.txt +1 -0
  86. pynode/nodes/JoinNode/__init__.py +3 -0
  87. pynode/nodes/JoinNode/join_node.py +156 -0
  88. pynode/nodes/JoinNode/requirements.txt +1 -0
  89. pynode/nodes/LabelFilterNode/__init__.py +3 -0
  90. pynode/nodes/LabelFilterNode/label_filter_node.py +188 -0
  91. pynode/nodes/LabelFilterNode/requirements.txt +1 -0
  92. pynode/nodes/MQTTNode/__init__.py +8 -0
  93. pynode/nodes/MQTTNode/mqtt_in_node.py +148 -0
  94. pynode/nodes/MQTTNode/mqtt_out_node.py +157 -0
  95. pynode/nodes/MQTTNode/mqtt_service.py +367 -0
  96. pynode/nodes/MQTTNode/requirements.txt +1 -0
  97. pynode/nodes/MessageWriterNode/__init__.py +7 -0
  98. pynode/nodes/MessageWriterNode/messagereader_node.py +568 -0
  99. pynode/nodes/MessageWriterNode/messagewriter_node.py +505 -0
  100. pynode/nodes/OmronCameraNode/__init__.py +3 -0
  101. pynode/nodes/OmronCameraNode/omron_camera_node.py +652 -0
  102. pynode/nodes/OmronCameraNode/requirements.txt +4 -0
  103. pynode/nodes/OpenCV/__init__.py +1 -0
  104. pynode/nodes/OpenCV/arithmetic_node.py +376 -0
  105. pynode/nodes/OpenCV/bitwise_node.py +127 -0
  106. pynode/nodes/OpenCV/blend_node.py +127 -0
  107. pynode/nodes/OpenCV/blob_detector_node.py +233 -0
  108. pynode/nodes/OpenCV/blur_node.py +141 -0
  109. pynode/nodes/OpenCV/color_space_node.py +139 -0
  110. pynode/nodes/OpenCV/colormap_node.py +262 -0
  111. pynode/nodes/OpenCV/contours_node.py +258 -0
  112. pynode/nodes/OpenCV/denormalize_coords_node.py +365 -0
  113. pynode/nodes/OpenCV/draw_node.py +312 -0
  114. pynode/nodes/OpenCV/edge_detector_node.py +177 -0
  115. pynode/nodes/OpenCV/equirectangular_node.py +447 -0
  116. pynode/nodes/OpenCV/fft_node.py +102 -0
  117. pynode/nodes/OpenCV/focus_estimation_node.py +189 -0
  118. pynode/nodes/OpenCV/frequency_filter_node.py +241 -0
  119. pynode/nodes/OpenCV/histogram_node.py +160 -0
  120. pynode/nodes/OpenCV/hough_circles_node.py +224 -0
  121. pynode/nodes/OpenCV/hough_lines_node.py +243 -0
  122. pynode/nodes/OpenCV/in_range_node.py +186 -0
  123. pynode/nodes/OpenCV/morphology_node.py +136 -0
  124. pynode/nodes/OpenCV/normalize_coords_node.py +344 -0
  125. pynode/nodes/OpenCV/paste_node.py +244 -0
  126. pynode/nodes/OpenCV/perspective_node.py +174 -0
  127. pynode/nodes/OpenCV/realsense_depth_node.py +253 -0
  128. pynode/nodes/OpenCV/requirements.txt +2 -0
  129. pynode/nodes/OpenCV/resize_node.py +175 -0
  130. pynode/nodes/OpenCV/rotate_node.py +159 -0
  131. pynode/nodes/OpenCV/template_match_node.py +227 -0
  132. pynode/nodes/OpenCV/threshold_node.py +167 -0
  133. pynode/nodes/PointInShapeNode/__init__.py +3 -0
  134. pynode/nodes/PointInShapeNode/point_in_shape_node.py +313 -0
  135. pynode/nodes/PointInShapeNode/requirements.txt +2 -0
  136. pynode/nodes/PolygonMetricsNode/__init__.py +3 -0
  137. pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +331 -0
  138. pynode/nodes/PolygonMetricsNode/requirements.txt +2 -0
  139. pynode/nodes/QueueLengthProbeNode/__init__.py +3 -0
  140. pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +116 -0
  141. pynode/nodes/Qwen3VLMNode/__init__.py +3 -0
  142. pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +519 -0
  143. pynode/nodes/Qwen3VLMNode/requirements.txt +5 -0
  144. pynode/nodes/RESTEndpointNode/__init__.py +3 -0
  145. pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +491 -0
  146. pynode/nodes/RangeNode/__init__.py +3 -0
  147. pynode/nodes/RangeNode/range_node.py +115 -0
  148. pynode/nodes/RangeNode/requirements.txt +1 -0
  149. pynode/nodes/RateProbeNode/__init__.py +3 -0
  150. pynode/nodes/RateProbeNode/rate_probe_node.py +183 -0
  151. pynode/nodes/RateProbeNode/requirements.txt +1 -0
  152. pynode/nodes/RoboflowUploadNode/__init__.py +3 -0
  153. pynode/nodes/RoboflowUploadNode/requirements.txt +1 -0
  154. pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +229 -0
  155. pynode/nodes/SliceNode/README.md +149 -0
  156. pynode/nodes/SliceNode/__init__.py +5 -0
  157. pynode/nodes/SliceNode/merge_predictions_node.py +444 -0
  158. pynode/nodes/SliceNode/requirements.txt +3 -0
  159. pynode/nodes/SliceNode/slice_collector_node.py +370 -0
  160. pynode/nodes/SliceNode/slice_image_node.py +380 -0
  161. pynode/nodes/SplitNode/__init__.py +3 -0
  162. pynode/nodes/SplitNode/requirements.txt +1 -0
  163. pynode/nodes/SplitNode/split_node.py +155 -0
  164. pynode/nodes/SwitchNode/requirements.txt +1 -0
  165. pynode/nodes/SwitchNode/switch_node.py +259 -0
  166. pynode/nodes/SyncNode/__init__.py +3 -0
  167. pynode/nodes/SyncNode/sync_node.py +287 -0
  168. pynode/nodes/TemplateNode/requirements.txt +1 -0
  169. pynode/nodes/TemplateNode/template_node.py +86 -0
  170. pynode/nodes/TrackerNode/__init__.py +2 -0
  171. pynode/nodes/TrackerNode/supervision_tracker_node.py +248 -0
  172. pynode/nodes/TrackerNode/tracker_node.py +745 -0
  173. pynode/nodes/UltralyticsNode/__init__.py +4 -0
  174. pynode/nodes/UltralyticsNode/requirements.txt +2 -0
  175. pynode/nodes/UltralyticsNode/ultralytics_node.py +364 -0
  176. pynode/nodes/UnknownNode/__init__.py +93 -0
  177. pynode/nodes/VLLMNode/__init__.py +3 -0
  178. pynode/nodes/VLLMNode/vllm_node.py +345 -0
  179. pynode/nodes/VideoReaderNode/__init__.py +3 -0
  180. pynode/nodes/VideoReaderNode/video_reader_node.py +425 -0
  181. pynode/nodes/VideoWriterNode/__init__.py +3 -0
  182. pynode/nodes/VideoWriterNode/video_writer_node.py +441 -0
  183. pynode/nodes/WebhookNode/__init__.py +3 -0
  184. pynode/nodes/WebhookNode/webhook_node.py +591 -0
  185. pynode/nodes/__init__.py +109 -0
  186. pynode/nodes/base_node.py +562 -0
  187. pynode/nodes/image_utils.py +291 -0
  188. pynode/nodes/info.py +109 -0
  189. pynode/nodes/mDNSNode/__init__.py +13 -0
  190. pynode/nodes/mDNSNode/mdns_broadcast_node.py +214 -0
  191. pynode/nodes/mDNSNode/mdns_discovery_node.py +362 -0
  192. pynode/nodes/mDNSNode/mdns_manager.py +399 -0
  193. pynode/nodes/mDNSNode/requirements.txt +1 -0
  194. pynode/nodes/messages.py +81 -0
  195. pynode/server.py +187 -0
  196. pynode/static/images/favicon4.png +0 -0
  197. pynode/static/index.html +200 -0
  198. pynode/static/js/README.md +139 -0
  199. pynode/static/js/auth.js +89 -0
  200. pynode/static/js/clipboard.js +152 -0
  201. pynode/static/js/config.js +14 -0
  202. pynode/static/js/connections.js +844 -0
  203. pynode/static/js/debug.js +524 -0
  204. pynode/static/js/events.js +775 -0
  205. pynode/static/js/history.js +199 -0
  206. pynode/static/js/main.js +55 -0
  207. pynode/static/js/minimap.js +242 -0
  208. pynode/static/js/nodes.js +995 -0
  209. pynode/static/js/palette.js +255 -0
  210. pynode/static/js/properties.js +1270 -0
  211. pynode/static/js/selection.js +241 -0
  212. pynode/static/js/state.js +211 -0
  213. pynode/static/js/ui-utils.js +28 -0
  214. pynode/static/js/viewport.js +204 -0
  215. pynode/static/js/workflow.js +469 -0
  216. pynode/static/js/workflows.js +344 -0
  217. pynode/static/style.css +2751 -0
  218. pynode/workflow_engine.py +483 -0
  219. pynode/workflow_manager.py +440 -0
  220. pynode_flow-0.2.0.dist-info/METADATA +520 -0
  221. pynode_flow-0.2.0.dist-info/RECORD +227 -0
  222. pynode_flow-0.2.0.dist-info/WHEEL +5 -0
  223. pynode_flow-0.2.0.dist-info/entry_points.txt +3 -0
  224. pynode_flow-0.2.0.dist-info/licenses/LICENSE +21 -0
  225. pynode_flow-0.2.0.dist-info/scm_file_list.json +270 -0
  226. pynode_flow-0.2.0.dist-info/scm_version.json +8 -0
  227. pynode_flow-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,4 @@
1
+ from .ultralytics_node import UltralyticsNode
2
+
3
+ __all__ = ['UltralyticsNode']
4
+
@@ -0,0 +1,2 @@
1
+ torch>=1.10.0
2
+ ultralytics>=8.4.0
@@ -0,0 +1,364 @@
1
+ """
2
+ Ultralytics YOLO node - performs object detection on images.
3
+ Supports YOLO26, YOLO11, YOLOv8 and other YOLO models.
4
+ Receives image data from camera nodes and outputs inference results.
5
+ """
6
+
7
+ import logging
8
+ import os
9
+ from typing import Any, Dict, List
10
+ from pynode.nodes.base_node import BaseNode, Info, MessageKeys
11
+ import torch
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ _info = Info()
16
+ _info.add_text("Performs YOLO object detection on input images using Ultralytics (YOLO26, YOLO11, YOLOv8, etc.).")
17
+ _info.add_header("Inputs")
18
+ _info.add_bullets(("Input 0:", "Image (base64 encoded)"))
19
+ _info.add_header("Outputs")
20
+ _info.add_bullets(
21
+ ("Output 0:", "Detection results with:"),
22
+ )
23
+ _info.add_bullets(
24
+ ("payload.image:", "Annotated image (if draw_results enabled)"),
25
+ ("payload.detections:", "List of detections with class_id, class_name, confidence, bbox"),
26
+ ("payload.detection_count:", "Number of detected objects"),
27
+ ("payload.bbox_format:", "Bounding box format (xyxy)"),
28
+ )
29
+ _info.add_header("Configuration")
30
+ _info.add_bullets(
31
+ ("Model:", "YOLO variant (YOLO26, YOLO11, YOLOv8 - nano to extra large)"),
32
+ ("Device:", "CPU, CUDA GPU, or Intel OpenVINO device for inference"),
33
+ ("Confidence:", "Detection confidence threshold"),
34
+ ("IoU:", "Intersection over Union threshold for NMS"),
35
+ )
36
+
37
+
38
+ class UltralyticsNode(BaseNode):
39
+ """
40
+ Ultralytics YOLO node - performs object detection inference.
41
+ Supports YOLO26, YOLO11, YOLOv8 and other YOLO models.
42
+ Receives images and outputs detection results with optional visualization.
43
+ """
44
+ # Visual properties
45
+ display_name = 'YOLO'
46
+ info = str(_info)
47
+ icon = '🎯'
48
+ category = 'vision'
49
+ color = '#00D9FF'
50
+ border_color = '#00A8CC'
51
+ text_color = '#000000'
52
+ input_count = 1
53
+ output_count = 1
54
+
55
+ DEFAULT_CONFIG = {
56
+ 'model': 'yolo26n.pt',
57
+ 'confidence': '0.25',
58
+ 'iou': '0.45',
59
+ 'draw_results': 'true',
60
+ 'max_det': '300',
61
+ 'include_image': True,
62
+ 'include_predictions': True,
63
+ 'drop_messages': 'true' # Enable by default for YOLO to prevent queue buildup
64
+ }
65
+
66
+ @staticmethod
67
+ def _get_device_options() -> List[Dict[str, str]]:
68
+ """Get available hardware devices (CPU, CUDA, Intel OpenVINO) for the dropdown."""
69
+ devices = [{'value': 'cpu', 'label': 'CPU'}]
70
+
71
+ try:
72
+ if torch.cuda.is_available():
73
+ for i in range(torch.cuda.device_count()):
74
+ props = torch.cuda.get_device_properties(i)
75
+ memory_gb = props.total_memory / (1024**3)
76
+ label = f"CUDA:{i} - {props.name} ({memory_gb:.1f}GB)"
77
+ devices.append({'value': f'cuda:{i}', 'label': label})
78
+ except Exception as e:
79
+ logger.warning(f"Error detecting CUDA devices: {e}")
80
+
81
+ # Add Intel OpenVINO devices (enumerated via openvino when available;
82
+ # falls back to the static CPU/GPU/NPU list otherwise)
83
+ try:
84
+ from pynode.nodes.InferenceNode.InferenceEngine.device_detection import (
85
+ get_intel_device_options,
86
+ )
87
+ devices.extend(get_intel_device_options())
88
+ except Exception as e:
89
+ logger.warning(f"Error detecting Intel OpenVINO devices: {e}")
90
+ devices.extend([
91
+ {'value': 'intel:cpu', 'label': 'Intel CPU (OpenVINO)'},
92
+ {'value': 'intel:gpu', 'label': 'Intel GPU (OpenVINO)'},
93
+ {'value': 'intel:npu', 'label': 'Intel NPU (OpenVINO)'},
94
+ ])
95
+
96
+ return devices
97
+
98
+ def _resolve_configured_device(self) -> str:
99
+ """Return the configured device with plain 'intel:gpu' resolved to the
100
+ first detected GPU (e.g. 'intel:gpu.0') so OpenVINO targets a real
101
+ device instead of falling back to AUTO on multi-GPU systems."""
102
+ device = self.config.get('device', 'cpu')
103
+ try:
104
+ from pynode.nodes.InferenceNode.InferenceEngine.device_detection import (
105
+ resolve_intel_device,
106
+ )
107
+ return resolve_intel_device(device)
108
+ except Exception:
109
+ return device
110
+
111
+ # Property schema for the properties panel
112
+ @classmethod
113
+ def get_properties(cls):
114
+ """Dynamic properties with device detection."""
115
+ return [
116
+ {
117
+ 'name': 'model',
118
+ 'label': 'Model',
119
+ 'type': 'select',
120
+ 'options': [
121
+ {'value': 'yolo26n.pt', 'label': 'YOLO26 Nano (fastest, recommended)'},
122
+ {'value': 'yolo26s.pt', 'label': 'YOLO26 Small'},
123
+ {'value': 'yolo26m.pt', 'label': 'YOLO26 Medium'},
124
+ {'value': 'yolo26l.pt', 'label': 'YOLO26 Large'},
125
+ {'value': 'yolo26x.pt', 'label': 'YOLO26 Extra Large (most accurate)'},
126
+ {'value': 'yolo11n.pt', 'label': 'YOLO11 Nano'},
127
+ {'value': 'yolo11s.pt', 'label': 'YOLO11 Small'},
128
+ {'value': 'yolo11m.pt', 'label': 'YOLO11 Medium'},
129
+ {'value': 'yolo11l.pt', 'label': 'YOLO11 Large'},
130
+ {'value': 'yolo11x.pt', 'label': 'YOLO11 Extra Large'},
131
+ {'value': 'yolov8n.pt', 'label': 'YOLOv8 Nano (legacy)'},
132
+ {'value': 'yolov8s.pt', 'label': 'YOLOv8 Small (legacy)'},
133
+ {'value': 'yolov8m.pt', 'label': 'YOLOv8 Medium (legacy)'},
134
+ {'value': 'yolov8l.pt', 'label': 'YOLOv8 Large (legacy)'},
135
+ {'value': 'yolov8x.pt', 'label': 'YOLOv8 Extra Large (legacy)'}
136
+ ]
137
+ },
138
+ {
139
+ 'name': 'device',
140
+ 'label': 'Device',
141
+ 'type': 'select',
142
+ 'options': cls._get_device_options()
143
+ },
144
+ {
145
+ 'name': 'confidence',
146
+ 'label': 'Confidence Threshold',
147
+ 'type': 'text',
148
+ 'placeholder': '0.25'
149
+ },
150
+ {
151
+ 'name': 'iou',
152
+ 'label': 'IoU Threshold',
153
+ 'type': 'text',
154
+ 'placeholder': '0.45'
155
+ },
156
+ {
157
+ 'name': 'draw_results',
158
+ 'label': 'Draw Results on Image',
159
+ 'type': 'select',
160
+ 'options': [
161
+ {'value': 'true', 'label': 'Yes'},
162
+ {'value': 'false', 'label': 'No'}
163
+ ]
164
+ },
165
+ {
166
+ 'name': 'max_det',
167
+ 'label': 'Max Detections',
168
+ 'type': 'text',
169
+ 'placeholder': '300'
170
+ },
171
+ {
172
+ 'name': 'include_image',
173
+ 'label': 'Include Image in Output',
174
+ 'type': 'checkbox',
175
+ 'default': True
176
+ },
177
+ {
178
+ 'name': 'include_predictions',
179
+ 'label': 'Include Predictions in Output',
180
+ 'type': 'checkbox',
181
+ 'default': True
182
+ }
183
+ ]
184
+
185
+ properties = property(lambda self: self.get_properties())
186
+
187
+ @staticmethod
188
+ def _get_default_device():
189
+ """Get default device based on availability."""
190
+ return 'cuda:0' if torch.cuda.is_available() else 'cpu'
191
+
192
+ def __init__(self, node_id=None, name="yolo"):
193
+ super().__init__(node_id, name)
194
+ # Configure with defaults, then set device dynamically
195
+ config = self.DEFAULT_CONFIG.copy()
196
+ config['device'] = self._get_default_device()
197
+ self.configure(config)
198
+ self.model = None
199
+ self._model_loaded = False
200
+
201
+ def _load_model(self):
202
+ """Load the YOLO model (lazy loading on first use)."""
203
+ if self._model_loaded:
204
+ return
205
+
206
+ try:
207
+ from ultralytics import YOLO # type: ignore
208
+ model_name = self.config.get('model', 'yolov8n.pt')
209
+ device = self._resolve_configured_device()
210
+
211
+ if isinstance(device, str) and device.lower().startswith('intel:'):
212
+ # Intel OpenVINO path: torch's model.to() does not understand
213
+ # 'intel:*' device strings, so export the model to OpenVINO
214
+ # format once and load the exported model instead. The
215
+ # resolved 'intel:gpu.N' string is passed to predict() at
216
+ # inference time; ultralytics maps it to the OpenVINO device
217
+ # name (e.g. intel:gpu.0 -> GPU.0).
218
+ base_model = YOLO(model_name)
219
+ source_path = str(getattr(base_model, 'ckpt_path', None) or model_name)
220
+ openvino_model_path = os.path.splitext(source_path)[0] + '_openvino_model'
221
+ if not os.path.isdir(openvino_model_path):
222
+ logger.info(f"Exporting {model_name} to OpenVINO format: {openvino_model_path}")
223
+ openvino_model_path = base_model.export(format='openvino')
224
+ self.model = YOLO(openvino_model_path, task=base_model.task)
225
+ logger.info(
226
+ f"Loaded OpenVINO model from {openvino_model_path} "
227
+ f"targeting device: {device}"
228
+ )
229
+ else:
230
+ self.model = YOLO(model_name)
231
+ # Move model to specified device
232
+ self.model.to(device)
233
+ self._model_loaded = True
234
+ except ImportError:
235
+ error_msg = "ultralytics package not installed. Run: pip install ultralytics"
236
+ self.report_error(error_msg)
237
+ self.model = None
238
+ except Exception as e:
239
+ error_msg = f"Error loading model: {e}"
240
+ self.report_error(error_msg)
241
+ self.model = None
242
+
243
+ def configure(self, config: Dict[str, Any]):
244
+ """Override configure to reload the model when model or device changes."""
245
+ old_model = self.config.get('model') if hasattr(self, 'config') else None
246
+ old_device = self.config.get('device') if hasattr(self, 'config') else None
247
+ super().configure(config)
248
+
249
+ # Mark for lazy reload on next inference if model or device changed.
250
+ # (The previous code called _load_model() directly, which no-ops while
251
+ # _model_loaded is True - so model changes never actually reloaded.)
252
+ if old_model is not None and (
253
+ old_model != self.config.get('model') or
254
+ old_device != self.config.get('device')
255
+ ):
256
+ self._model_loaded = False
257
+
258
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
259
+ """
260
+ Process incoming image messages and perform YOLO inference.
261
+ """
262
+ # Lazy load model on first use
263
+ if not self._model_loaded:
264
+ self._load_model()
265
+
266
+ if self.model is None:
267
+ error_msg = "Model not loaded, skipping inference"
268
+ self.report_error(error_msg)
269
+ return
270
+
271
+ # Get image from message
272
+ payload = msg.get(MessageKeys.PAYLOAD)
273
+ if payload is None:
274
+ error_msg = "No image in payload"
275
+ self.report_error(error_msg)
276
+ return
277
+
278
+ # Decode image using base node helper
279
+ image, input_format = self.decode_image(payload)
280
+
281
+ if image is None or input_format is None:
282
+ error_msg = "Failed to decode image from payload"
283
+ self.report_error(error_msg)
284
+ return
285
+
286
+ try:
287
+ # Get inference parameters
288
+ confidence = self.get_config_float('confidence', 0.25)
289
+ iou = self.get_config_float('iou', 0.45)
290
+ max_det = self.get_config_int('max_det', 300)
291
+ draw_results = self.get_config_bool('draw_results', True)
292
+
293
+ # Perform inference on specified device ('intel:gpu' resolved to
294
+ # the first detected GPU, e.g. 'intel:gpu.0')
295
+ device = self._resolve_configured_device()
296
+ results = self.model.predict(
297
+ image,
298
+ conf=confidence,
299
+ iou=iou,
300
+ max_det=max_det,
301
+ device=device,
302
+ verbose=False
303
+ )
304
+
305
+ # Extract detection information. Pull the whole batch off the device
306
+ # once (each per-box .cpu() call is a separate sync + thread-pool
307
+ # wake-up) and iterate the resulting numpy arrays.
308
+ detections = []
309
+ if len(results) > 0:
310
+ result = results[0]
311
+ boxes = result.boxes
312
+
313
+ if boxes is not None and len(boxes) > 0:
314
+ xyxy = boxes.xyxy.cpu().numpy()
315
+ cls = boxes.cls.cpu().numpy().astype(int)
316
+ conf = boxes.conf.cpu().numpy()
317
+ for bbox, class_id, confidence_score in zip(xyxy, cls, conf):
318
+ detection = {
319
+ 'class_id': int(class_id),
320
+ 'class_name': result.names[int(class_id)],
321
+ 'confidence': float(confidence_score),
322
+ 'bbox': bbox.tolist(), # [x1, y1, x2, y2]
323
+ 'bbox_format': 'xyxy'
324
+ }
325
+ detections.append(detection)
326
+
327
+ # Prepare output image
328
+ output_image = image
329
+ if draw_results and len(results) > 0:
330
+ output_image = results[0].plot()
331
+
332
+ # Encode image back to same format as input
333
+ include_image = self.config.get('include_image', True)
334
+ include_predictions = self.config.get('include_predictions', True)
335
+
336
+ payload_out = {}
337
+
338
+ if include_image:
339
+ # Encode image back to same format as input using base node helper
340
+ encoded_image = self.encode_image(output_image, input_format)
341
+ if encoded_image is not None:
342
+ payload_out[MessageKeys.IMAGE.PATH] = encoded_image
343
+ else:
344
+ self.report_error("Failed to encode output image")
345
+ return
346
+
347
+ # Always include detections and detection_count if include_predictions is True (for backward compatibility)
348
+ if include_predictions or True:
349
+ payload_out['detections'] = detections
350
+ payload_out['detection_count'] = len(detections)
351
+ payload_out['bbox_format'] = 'xyxy' # Document the bbox format at payload level
352
+
353
+ # Preserve original message properties (like frame_count) and update payload
354
+ # Note: send() handles deep copying, so we modify msg directly
355
+ msg[MessageKeys.PAYLOAD] = payload_out
356
+ msg[MessageKeys.TOPIC] = msg.get(MessageKeys.TOPIC, 'yolo')
357
+
358
+ # Send the message
359
+ self.send(msg)
360
+
361
+ except Exception as e:
362
+ error_msg = f"Error during inference: {e}"
363
+ self.report_error(error_msg)
364
+
@@ -0,0 +1,93 @@
1
+ """
2
+ Unknown/Placeholder Node - represents a missing node type in the workflow.
3
+ """
4
+
5
+ from pynode.nodes.base_node import BaseNode
6
+
7
+
8
+ class UnknownNode(BaseNode):
9
+ """
10
+ Placeholder node for unknown/missing node types.
11
+
12
+ This node is created when loading a workflow that contains a node type
13
+ that is not currently registered (e.g., the node was removed or renamed).
14
+
15
+ The node preserves the original configuration and type information so the
16
+ workflow can still be loaded and the user can repair/replace the node.
17
+ """
18
+
19
+ display_name = "Unknown"
20
+ icon = "❓"
21
+ category = "system"
22
+ color = "#3d3d3d"
23
+ border_color = "#ff6b6b"
24
+ text_color = "#999"
25
+ # input_count and output_count are handled via properties below
26
+ hidden = True # Don't show in palette - this is only created automatically
27
+
28
+ properties = [
29
+ {
30
+ 'name': 'original_type',
31
+ 'label': 'Original Type',
32
+ 'type': 'text',
33
+ 'default': '',
34
+ 'readonly': True
35
+ },
36
+ {
37
+ 'name': 'original_config',
38
+ 'label': 'Original Config',
39
+ 'type': 'json',
40
+ 'default': {},
41
+ 'readonly': True
42
+ }
43
+ ]
44
+
45
+ info = """
46
+ <p>This is a placeholder for a node type that could not be found.</p>
47
+ <p>The original node type was not available when loading the workflow.</p>
48
+ <h4>Possible reasons:</h4>
49
+ <ul>
50
+ <li>The node was removed or renamed</li>
51
+ <li>A required dependency is not installed</li>
52
+ <li>The workflow was created with a different version</li>
53
+ </ul>
54
+ <h4>To fix:</h4>
55
+ <p>Delete this node and replace it with an appropriate alternative,
56
+ or install the missing node type.</p>
57
+ """
58
+
59
+ def __init__(self, node_id=None, name="Unknown", original_type=None,
60
+ original_config=None, input_count=1, output_count=1):
61
+ super().__init__(node_id=node_id, name=name)
62
+ self.config['original_type'] = original_type or ''
63
+ self.config['original_config'] = original_config or {}
64
+ # Store the original port counts so connections can still be made
65
+ self._original_input_count = input_count
66
+ self._original_output_count = output_count
67
+ # Mark as disabled since it can't function
68
+ self.enabled = False
69
+ # Mark as unknown for frontend styling
70
+ self._is_unknown_node = True
71
+
72
+ @property
73
+ def input_count(self):
74
+ """Return the original input count to preserve connections."""
75
+ return getattr(self, '_original_input_count', 1)
76
+
77
+ @property
78
+ def output_count(self):
79
+ """Return the original output count to preserve connections."""
80
+ return getattr(self, '_original_output_count', 1)
81
+
82
+ def process(self, msg):
83
+ """Unknown nodes don't process messages - they're just placeholders."""
84
+ # Don't forward messages since we don't know what this node should do
85
+ return None
86
+
87
+ def on_start(self):
88
+ """No-op for unknown nodes."""
89
+ pass
90
+
91
+ def on_stop(self):
92
+ """No-op for unknown nodes."""
93
+ pass
@@ -0,0 +1,3 @@
1
+ from .vllm_node import VLLMNode
2
+
3
+ __all__ = ['VLLMNode']