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,3 @@
1
+ from .filter_node import FilterNode
2
+
3
+ __all__ = ['FilterNode']
@@ -0,0 +1,102 @@
1
+ """
2
+ Filter Node - filters messages based on conditions.
3
+ """
4
+
5
+ from typing import Any, Dict
6
+ from pynode.nodes.base_node import BaseNode, Info, MessageKeys
7
+
8
+ _info = Info()
9
+ _info.add_text("Filters messages based on various conditions. Only passes messages that meet the specified criteria.")
10
+ _info.add_header("Inputs")
11
+ _info.add_bullets(
12
+ ("Input 0:", "Any message to be filtered.")
13
+ )
14
+ _info.add_header("Outputs")
15
+ _info.add_bullets(
16
+ ("Output 0:", "Messages that pass the filter.")
17
+ )
18
+ _info.add_header("Filter Modes")
19
+ _info.add_bullets(
20
+ ("Block:", "Block unless payload value changes."),
21
+ ("Dedupe:", "Block duplicate payloads (same as Block)."),
22
+ ("Drop First:", "Drop the first N messages, pass the rest."),
23
+ ("Keep First:", "Keep only the first N messages, drop the rest.")
24
+ )
25
+
26
+
27
+ class FilterNode(BaseNode):
28
+ """
29
+ Filter Node - only passes messages that meet specified conditions.
30
+ """
31
+ display_name = 'Filter'
32
+ icon = '🔍'
33
+ category = 'function'
34
+ color = '#FFB6C1'
35
+ border_color = '#FF69B4'
36
+ text_color = '#000000'
37
+ input_count = 1
38
+ output_count = 1
39
+ info = str(_info)
40
+
41
+ properties = [
42
+ {
43
+ 'name': 'mode',
44
+ 'label': 'Mode',
45
+ 'type': 'select',
46
+ 'options': [
47
+ {'value': 'block', 'label': 'Block unless value changes'},
48
+ {'value': 'dedupe', 'label': 'Deduplicate (block duplicates)'},
49
+ {'value': 'drop_first', 'label': 'Drop first N messages'},
50
+ {'value': 'keep_first', 'label': 'Keep only first N messages'}
51
+ ]
52
+ },
53
+ {
54
+ 'name': 'count',
55
+ 'label': 'Count',
56
+ 'type': 'number',
57
+ 'default': 1
58
+ }
59
+ ]
60
+
61
+ def __init__(self, node_id=None, name="filter"):
62
+ super().__init__(node_id, name)
63
+ self.configure({
64
+ 'mode': 'dedupe',
65
+ 'count': 1,
66
+ 'drop_messages': False
67
+ })
68
+ self.last_value = None
69
+ self.message_count = 0
70
+
71
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
72
+ """Filter messages based on configured mode."""
73
+ mode = self.config.get('mode', 'dedupe')
74
+ payload = msg.get(MessageKeys.PAYLOAD)
75
+
76
+ should_send = False
77
+
78
+ if mode == 'block':
79
+ # Block unless value changes
80
+ if payload != self.last_value:
81
+ should_send = True
82
+ self.last_value = payload
83
+ elif mode == 'dedupe':
84
+ # Deduplicate
85
+ if payload != self.last_value:
86
+ should_send = True
87
+ self.last_value = payload
88
+ elif mode == 'drop_first':
89
+ # Drop first N messages
90
+ count = self.get_config_int('count', 1)
91
+ self.message_count += 1
92
+ if self.message_count > count:
93
+ should_send = True
94
+ elif mode == 'keep_first':
95
+ # Keep only first N messages
96
+ count = self.get_config_int('count', 1)
97
+ self.message_count += 1
98
+ if self.message_count <= count:
99
+ should_send = True
100
+
101
+ if should_send:
102
+ self.send(msg)
@@ -0,0 +1 @@
1
+ # No external dependencies - uses only Python standard library
@@ -0,0 +1,4 @@
1
+ from .frame_source_node import FrameSourceNode
2
+
3
+ __all__ = ['FrameSourceNode']
4
+
@@ -0,0 +1,319 @@
1
+ """
2
+ Frame Source node - captures frames from various video sources.
3
+ """
4
+
5
+ import cv2
6
+ import base64
7
+ import os
8
+ import threading
9
+ import time
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Any, Dict
13
+ from pynode.nodes.base_node import BaseNode, Info, MessageKeys
14
+
15
+ _info = Info()
16
+ _info.add_text("Captures frames from various video sources and outputs them as messages. Supports webcams, video files, RTSP streams, image folders, and specialized cameras.")
17
+ _info.add_header("Outputs")
18
+ _info.add_bullets(
19
+ ("Output 0:", "Frame message with image data (numpy array or JPEG encoded).")
20
+ )
21
+ _info.add_header("Source Types")
22
+ _info.add_bullets(
23
+ ("Webcam:", "Local USB webcam (source = device index, e.g. 0)"),
24
+ ("Video File:", "MP4, AVI, or other video files"),
25
+ ("RTSP Stream:", "IP camera RTSP URL"),
26
+ ("Image Folder:", "Directory of images to cycle through"),
27
+ ("Basler:", "Basler industrial camera"),
28
+ ("RealSense:", "Intel RealSense depth camera (includes depth data)"),
29
+ ("Screen Capture:", "Desktop screen recording"),
30
+ ("GenICam:", "GenICam-compatible industrial cameras"),
31
+ ("Audio Spectrogram:", "Audio visualization as frames")
32
+ )
33
+ _info.add_header("Output Format")
34
+ _info.add_bullets(
35
+ (f"{MessageKeys.PAYLOAD}.image:", "Image data as numpy array or base64 JPEG"),
36
+ (f"{MessageKeys.PAYLOAD}.frame_count:", "Frame sequence number"),
37
+ (f"{MessageKeys.PAYLOAD}.width/height:", "Frame dimensions"),
38
+ (f"{MessageKeys.PAYLOAD}.loop:", "Indicates if the video/folder is looping")
39
+ )
40
+
41
+ # Add FrameSource package to path
42
+ _framesource_path = Path(__file__).parent / 'FrameSource'
43
+ if str(_framesource_path) not in sys.path:
44
+ sys.path.insert(0, str(_framesource_path))
45
+
46
+ from frame_source import FrameSourceFactory
47
+
48
+
49
+ class FrameSourceNode(BaseNode):
50
+ """
51
+ Frame Source node - captures frames from a webcam and outputs them as messages.
52
+ """
53
+ display_name = 'Frame Source'
54
+ icon = '📷'
55
+ category = 'input'
56
+ color = '#C0DEED'
57
+ border_color = '#7FA7C9'
58
+ text_color = '#000000'
59
+ input_count = 0 # No input
60
+ output_count = 1
61
+ info = str(_info)
62
+
63
+ api_routes = [
64
+ {
65
+ 'route': 'upload_video',
66
+ 'methods': ['POST'],
67
+ 'handler': 'handle_upload_video',
68
+ 'type': 'file_upload',
69
+ 'allowed_extensions': {'.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.webm', '.m4v', '.mpg', '.mpeg'},
70
+ },
71
+ ]
72
+
73
+ DEFAULT_CONFIG = {
74
+ MessageKeys.CAMERA.SOURCE_TYPE: 'webcam',
75
+ MessageKeys.CAMERA.SOURCE: 0,
76
+ MessageKeys.CAMERA.FPS: 30,
77
+ MessageKeys.CAMERA.WIDTH: 640,
78
+ MessageKeys.CAMERA.HEIGHT: 480,
79
+ MessageKeys.CAMERA.ENCODE_JPEG: False,
80
+ MessageKeys.CAMERA.JPEG_QUALITY: 75,
81
+ MessageKeys.VIDEO.LOOP: True
82
+ }
83
+
84
+ properties = [
85
+ {
86
+ 'name': MessageKeys.CAMERA.SOURCE_TYPE,
87
+ 'label': 'Source Type',
88
+ 'type': 'select',
89
+ 'options': [
90
+ {'value': 'webcam', 'label': 'Webcam'},
91
+ {'value': 'video_file', 'label': 'Video File'},
92
+ {'value': 'ipcam', 'label': 'RTSP Stream'},
93
+ {'value': 'folder', 'label': 'Image Folder'},
94
+ {'value': 'basler', 'label': 'Basler Camera'},
95
+ {'value': 'realsense', 'label': 'RealSense'},
96
+ {'value': 'screen', 'label': 'Screen Capture'},
97
+ {'value': 'genicam', 'label': 'GenICam Camera'},
98
+ {'value': 'audio_spectrogram', 'label': 'Audio Spectrogram'}
99
+ ],
100
+ 'default': DEFAULT_CONFIG[MessageKeys.CAMERA.SOURCE_TYPE],
101
+ },
102
+ {
103
+ 'name': MessageKeys.CAMERA.SOURCE,
104
+ 'label': 'Video File',
105
+ 'type': 'file',
106
+ 'accept': '.mp4,.avi,.mkv,.mov,.wmv,.flv,.webm,.m4v,.mpg,.mpeg',
107
+ 'uploadRoute': 'upload_video',
108
+ 'placeholder': 'Upload or enter video file path...',
109
+ 'showIf': {MessageKeys.CAMERA.SOURCE_TYPE: 'video_file'},
110
+ },
111
+ {
112
+ 'name': MessageKeys.CAMERA.SOURCE,
113
+ 'label': 'Source',
114
+ 'type': 'text',
115
+ 'default': str(DEFAULT_CONFIG[MessageKeys.CAMERA.SOURCE]),
116
+ 'showIf': {MessageKeys.CAMERA.SOURCE_TYPE: ['webcam', 'ipcam', 'folder', 'basler', 'realsense', 'screen', 'genicam', 'audio_spectrogram']},
117
+ },
118
+ {
119
+ 'name': MessageKeys.CAMERA.FPS,
120
+ 'label': 'Frame Rate (FPS)',
121
+ 'type': 'number',
122
+ 'default': DEFAULT_CONFIG[MessageKeys.CAMERA.FPS]
123
+ },
124
+ {
125
+ 'name': MessageKeys.VIDEO.LOOP,
126
+ 'label': 'Loop Video/Folder',
127
+ 'type': 'checkbox',
128
+ 'default': DEFAULT_CONFIG[MessageKeys.VIDEO.LOOP],
129
+ 'showIf': {MessageKeys.CAMERA.SOURCE_TYPE: ['video_file', 'folder']}
130
+ },
131
+ {
132
+ 'name': MessageKeys.CAMERA.WIDTH,
133
+ 'label': 'Width',
134
+ 'type': 'number',
135
+ 'default': DEFAULT_CONFIG[MessageKeys.CAMERA.WIDTH]
136
+ },
137
+ {
138
+ 'name': MessageKeys.CAMERA.HEIGHT,
139
+ 'label': 'Height',
140
+ 'type': 'number',
141
+ 'default': DEFAULT_CONFIG[MessageKeys.CAMERA.HEIGHT]
142
+ },
143
+ {
144
+ 'name': MessageKeys.CAMERA.ENCODE_JPEG,
145
+ 'label': 'Encode as JPEG',
146
+ 'type': 'checkbox',
147
+ 'default': DEFAULT_CONFIG[MessageKeys.CAMERA.ENCODE_JPEG]
148
+ },
149
+ {
150
+ 'name': MessageKeys.CAMERA.JPEG_QUALITY,
151
+ 'label': 'JPEG Quality (1-100)',
152
+ 'type': 'number',
153
+ 'default': DEFAULT_CONFIG[MessageKeys.CAMERA.JPEG_QUALITY]
154
+ }
155
+ ]
156
+
157
+ def __init__(self, node_id=None, name="camera"):
158
+ super().__init__(node_id, name)
159
+ self.camera = None
160
+ self.capture_thread = None
161
+ self.running = False
162
+ self.frame_count = 0
163
+
164
+ def handle_upload_video(self, file_bytes, filename):
165
+ """Handle video file upload via the dynamic API route."""
166
+ try:
167
+ # Save to a videos directory next to this node
168
+ videos_dir = os.path.join(os.path.dirname(__file__), 'videos')
169
+ os.makedirs(videos_dir, exist_ok=True)
170
+
171
+ file_path = os.path.join(videos_dir, os.path.basename(filename))
172
+ with open(file_path, 'wb') as f:
173
+ f.write(file_bytes)
174
+
175
+ return {
176
+ 'success': True,
177
+ 'file_path': file_path,
178
+ 'filename': filename
179
+ }
180
+ except Exception as e:
181
+ return {
182
+ 'success': False,
183
+ 'error': str(e)
184
+ }
185
+
186
+ def on_start(self):
187
+ """Start the camera capture when workflow starts."""
188
+ super().on_start() # Start base node worker thread
189
+
190
+ source_type = self.config.get(MessageKeys.CAMERA.SOURCE_TYPE, 'webcam')
191
+ source = self.config.get(MessageKeys.CAMERA.SOURCE, 0)
192
+ fps = self.get_config_int(MessageKeys.CAMERA.FPS, 30)
193
+ width = self.get_config_int(MessageKeys.CAMERA.WIDTH, 640)
194
+ height = self.get_config_int(MessageKeys.CAMERA.HEIGHT, 480)
195
+ loop = self.get_config_bool(MessageKeys.VIDEO.LOOP, False)
196
+
197
+ try:
198
+ # Create camera with config including resolution and fps
199
+ self.camera = FrameSourceFactory.create(
200
+ source_type,
201
+ source=source,
202
+ width=width,
203
+ height=height,
204
+ fps=fps,
205
+ loop=loop
206
+ )
207
+
208
+ if not self.camera.connect():
209
+ self.report_error(f"Failed to connect to {source_type} source: {source}")
210
+ return
211
+
212
+ self.frame_count = 0 # Reset frame counter on start
213
+
214
+ # Start capture thread
215
+ self.running = True
216
+ self.capture_thread = threading.Thread(target=self._capture_loop, args=(fps,), daemon=True)
217
+ self.capture_thread.start()
218
+
219
+ except Exception as e:
220
+ self.report_error(f"Error starting camera: {e}")
221
+
222
+ def on_stop(self):
223
+ """Stop the camera capture when workflow stops."""
224
+ super().on_stop() # Stop base node worker thread
225
+
226
+ self.running = False
227
+
228
+ if self.capture_thread:
229
+ self.capture_thread.join(timeout=2.0)
230
+ self.capture_thread = None
231
+
232
+ if self.camera:
233
+ self.camera.release()
234
+ self.camera = None
235
+
236
+ def on_close(self):
237
+ """Cleanup when node is deleted."""
238
+ self.on_stop()
239
+
240
+ def _capture_loop(self, fps):
241
+ """Capture frames in a loop and send them as messages."""
242
+ frame_interval = 1.0 / fps
243
+ encode_jpeg = self.config.get(MessageKeys.CAMERA.ENCODE_JPEG, False)
244
+
245
+ while self.running and self.camera and self.camera.isOpened():
246
+ start_time = time.time()
247
+
248
+ try:
249
+ ret, frame = self.camera.read()
250
+
251
+ # Check if frame has 4 channels (RGBD data)
252
+ if ret and frame is not None and len(frame.shape) == 3 and frame.shape[2] == 4:
253
+ # Split RGB and depth channels
254
+ rgb_frame = frame[:, :, :3]
255
+ depth_channel = frame[:, :, 3]
256
+ frame = rgb_frame
257
+ has_depth = True
258
+ else:
259
+ has_depth = False
260
+ depth_channel = None
261
+
262
+ if not ret or frame is None:
263
+ self.report_error("Failed to capture frame")
264
+ time.sleep(frame_interval)
265
+ continue
266
+
267
+ # Prepare the payload
268
+ if encode_jpeg:
269
+ # Encode frame as JPEG with quality setting
270
+ jpeg_quality = self.get_config_int(MessageKeys.IMAGE.JPEG_QUALITY, 75)
271
+ encode_params = [cv2.IMWRITE_JPEG_QUALITY, jpeg_quality]
272
+ ret, buffer = cv2.imencode('.jpg', frame, encode_params)
273
+ if ret:
274
+ # Convert to bytes (more efficient than base64)
275
+ jpeg_bytes = buffer.tobytes()
276
+ # Convert to base64 for JSON transmission
277
+ jpeg_base64 = base64.b64encode(jpeg_bytes).decode('utf-8')
278
+ payload = {
279
+ MessageKeys.IMAGE.FORMAT: 'jpeg',
280
+ MessageKeys.IMAGE.ENCODING: 'base64',
281
+ MessageKeys.IMAGE.DATA: jpeg_base64,
282
+ MessageKeys.IMAGE.WIDTH: frame.shape[1],
283
+ MessageKeys.IMAGE.HEIGHT: frame.shape[0]
284
+ }
285
+ else:
286
+ self.report_error("Failed to encode JPEG")
287
+ continue
288
+ else:
289
+ # Send raw frame as numpy array
290
+ payload = {
291
+ MessageKeys.IMAGE.FORMAT: 'bgr',
292
+ MessageKeys.IMAGE.ENCODING: 'numpy',
293
+ MessageKeys.IMAGE.DATA: frame,
294
+ MessageKeys.IMAGE.WIDTH: frame.shape[1],
295
+ MessageKeys.IMAGE.HEIGHT: frame.shape[0]
296
+ }
297
+
298
+ # Create message payload
299
+ message_payload: Dict[str, Any] = {MessageKeys.IMAGE.PATH: payload}
300
+
301
+ # Add depth data if available (compatible with RealsenseDepthNode)
302
+ if has_depth:
303
+ message_payload['depth'] = depth_channel
304
+
305
+ # Increment frame counter
306
+ self.frame_count += 1
307
+
308
+ # Create and send message
309
+ msg = self.create_message(payload=message_payload, topic='camera/frame', frame_count=self.frame_count)
310
+ self.send(msg)
311
+
312
+ except Exception as e:
313
+ self.report_error(f"Error capturing frame: {e}")
314
+
315
+ # Maintain frame rate
316
+ elapsed = time.time() - start_time
317
+ sleep_time = max(0, frame_interval - elapsed)
318
+ if sleep_time > 0:
319
+ time.sleep(sleep_time)
@@ -0,0 +1 @@
1
+ framesource[full]
@@ -0,0 +1,3 @@
1
+ from .function_node import FunctionNode
2
+
3
+ __all__ = ['FunctionNode']
@@ -0,0 +1,168 @@
1
+ """
2
+ Function node - executes custom Python code on messages.
3
+ Similar to Node-RED's function node.
4
+ """
5
+
6
+ import time
7
+ import copy
8
+ from typing import Any, Dict
9
+ from pynode.nodes.base_node import BaseNode, Info, MessageKeys
10
+
11
+ _info = Info()
12
+ _info.add_text("Executes custom Python code on incoming messages. Similar to Node-RED's function node.")
13
+ _info.add_header("Inputs")
14
+ _info.add_bullets(
15
+ ("Input 0:", "Any message to process.")
16
+ )
17
+ _info.add_header("Outputs")
18
+ _info.add_bullets(
19
+ ("Output 0-N:", "Processed message(s). Configure number of outputs in properties.")
20
+ )
21
+ _info.add_header("Available Variables")
22
+ _info.add_bullets(
23
+ ("msg:", "The incoming message dictionary."),
24
+ ("node:", "Reference to this node (for node.send(), node.report_error())."),
25
+ ("time:", "Python time module.")
26
+ )
27
+ _info.add_header("Example")
28
+ _info.add_code(f"msg['{MessageKeys.PAYLOAD}'] = msg['{MessageKeys.PAYLOAD}'] * 2\nreturn msg").text("Doubles the payload value and returns the modified message.").end()
29
+ _info.add_header("Multiple Outputs")
30
+ _info.add_code(f"return [msg, None, {{'{MessageKeys.PAYLOAD}': 'alt'}}]").text("Returns array to send to different outputs. None skips that output.").end()
31
+
32
+
33
+ class FunctionNode(BaseNode):
34
+ """
35
+ Function node - executes custom Python code on messages.
36
+ Similar to Node-RED's function node.
37
+ """
38
+ display_name = 'Function'
39
+ icon = 'ƒ'
40
+ category = 'function'
41
+ color = '#E6E0F8'
42
+ border_color = '#9F93C6'
43
+ text_color = '#000000'
44
+ input_count = 1
45
+ output_count = 1
46
+ info = str(_info)
47
+
48
+ properties = [
49
+ {
50
+ 'name': 'func',
51
+ 'label': 'Function',
52
+ 'type': 'textarea'
53
+ },
54
+ {
55
+ 'name': 'outputs',
56
+ 'label': 'Outputs',
57
+ 'type': 'number',
58
+ 'default': 1,
59
+ 'min': 1,
60
+ 'max': 10
61
+ }
62
+ ]
63
+
64
+ def __init__(self, node_id=None, name="function"):
65
+ super().__init__(node_id, name)
66
+
67
+ def configure(self, config: Dict[str, Any]):
68
+ """Configure the node and update output_count based on outputs setting."""
69
+ super().configure(config)
70
+ self.output_count = self.get_config_int('outputs', 1)
71
+
72
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
73
+ """
74
+ Execute the function code on the incoming message.
75
+ """
76
+ try:
77
+ func_code = self.config.get('func', 'return msg')
78
+
79
+ # Wrap the user's code in a function to support return statements
80
+ wrapped_code = f'''def user_function(msg, node, time):
81
+ {chr(10).join(" " + line for line in func_code.split(chr(10)))}
82
+
83
+ result = user_function(msg, node, time)
84
+ '''
85
+
86
+ # Create a deep copy to avoid modifying the original and handle non-serializable objects
87
+ try:
88
+ msg_copy = copy.deepcopy(msg)
89
+ except Exception:
90
+ # If deep copy fails, use shallow copy and let user handle it
91
+ msg_copy = msg.copy()
92
+
93
+ # Create a safe execution context with helpful utilities
94
+ context = {
95
+ 'msg': msg_copy,
96
+ 'node': self,
97
+ 'time': time,
98
+ # Add common utilities
99
+ 'isinstance': isinstance,
100
+ 'dict': dict,
101
+ 'list': list,
102
+ 'str': str,
103
+ 'int': int,
104
+ 'float': float,
105
+ 'set': set,
106
+ 'tuple': tuple
107
+ }
108
+
109
+ # Execute the wrapped function
110
+ exec(wrapped_code, context)
111
+
112
+ # Get the result
113
+ result = context.get('result')
114
+
115
+ if result is not None:
116
+ if isinstance(result, list):
117
+ # Multiple outputs
118
+ for idx, out_msg in enumerate(result):
119
+ if out_msg is not None:
120
+ self.send(out_msg, idx)
121
+ else:
122
+ # Single output
123
+ self.send(result)
124
+
125
+ except SyntaxError as e:
126
+ # Provide helpful syntax error messages with line numbers
127
+ line_num = e.lineno - 1 if e.lineno else 0 # Adjust for wrapper function
128
+ user_lines = func_code.split('\n')
129
+
130
+ error_context = f"Syntax error on line {line_num}: {e.msg}\n"
131
+ if 0 <= line_num - 1 < len(user_lines):
132
+ error_context += f" {line_num}: {user_lines[line_num - 1]}\n"
133
+ if 0 <= line_num < len(user_lines):
134
+ error_context += f"→ {line_num + 1}: {user_lines[line_num]}\n"
135
+ if line_num + 1 < len(user_lines):
136
+ error_context += f" {line_num + 2}: {user_lines[line_num + 1]}\n"
137
+
138
+ self.report_error(error_context)
139
+ error_msg = self.create_message(
140
+ payload={'error': error_context, 'type': 'SyntaxError'},
141
+ topic='error'
142
+ )
143
+ self.send(error_msg)
144
+
145
+ except Exception as e:
146
+ # Provide more helpful error messages
147
+ error_str = str(e)
148
+ payload_type = type(msg.get(MessageKeys.PAYLOAD)).__name__
149
+
150
+ # Add context-specific hints
151
+ if "does not support item assignment" in error_str:
152
+ hint = f"Hint: msg['{MessageKeys.PAYLOAD}'] is a {payload_type}, not a dict. "
153
+ if payload_type in ['float', 'int', 'str', 'bool']:
154
+ hint += f"To add an index, wrap it: msg['{MessageKeys.PAYLOAD}'] = {{'value': msg['{MessageKeys.PAYLOAD}'], 'index': 0}}"
155
+ error_str = hint + " Error: " + error_str
156
+
157
+ # Report error to the error system
158
+ self.report_error(f"Function error: {error_str}")
159
+ # Also send error downstream for debugging
160
+ error_msg = self.create_message(
161
+ payload={
162
+ 'error': error_str,
163
+ 'original_payload': msg.get(MessageKeys.PAYLOAD),
164
+ 'payload_type': payload_type
165
+ },
166
+ topic='error'
167
+ )
168
+ self.send(error_msg)
@@ -0,0 +1 @@
1
+ # No external dependencies - uses only Python standard library
@@ -0,0 +1,60 @@
1
+ """
2
+ Gate node - allows messages to pass through or blocks them.
3
+ Can be toggled on/off directly from the UI without redeployment.
4
+ """
5
+
6
+ from typing import Any, Dict
7
+ from pynode.nodes.base_node import BaseNode, Info, MessageKeys
8
+
9
+ _info = Info()
10
+ _info.add_text("Allows or blocks messages from passing through. Can be toggled on/off in real-time from the UI without redeploying.")
11
+ _info.add_header("Inputs")
12
+ _info.add_bullets(
13
+ ("Input 0:", "Any message.")
14
+ )
15
+ _info.add_header("Outputs")
16
+ _info.add_bullets(
17
+ ("Output 0:", "Messages pass through when gate is open.")
18
+ )
19
+ _info.add_header("Usage")
20
+ _info.add_bullets(
21
+ ("Toggle:", "Click the toggle button on the node to open/close the gate."),
22
+ ("Open:", "Messages pass through unchanged."),
23
+ ("Closed:", "Messages are silently discarded.")
24
+ )
25
+
26
+
27
+ class GateNode(BaseNode):
28
+ """
29
+ Gate node - allows or blocks messages based on enabled state.
30
+ Can be toggled in real-time from the UI.
31
+ """
32
+ display_name = 'Gate'
33
+ icon = '🚪'
34
+ category = 'logic'
35
+ color = '#FFE5B4'
36
+ border_color = '#FFD700'
37
+ text_color = '#000000'
38
+ input_count = 1
39
+ output_count = 1
40
+ info = str(_info)
41
+ ui_component = 'toggle'
42
+ ui_component_config = {
43
+ 'action': 'toggle_gate',
44
+ 'label': 'Open'
45
+ }
46
+
47
+ properties = [] # No properties panel needed
48
+
49
+ def __init__(self, node_id=None, name="gate"):
50
+ super().__init__(node_id, name)
51
+ self.enabled = True # Gate is open (enabled) by default
52
+ self.configure({})
53
+
54
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
55
+ """
56
+ Pass message through if gate is enabled, otherwise discard it.
57
+ """
58
+ if self.enabled:
59
+ self.send(msg)
60
+ # If gate is disabled, message is silently discarded
@@ -0,0 +1 @@
1
+ # No external dependencies - uses only Python standard library
@@ -0,0 +1,3 @@
1
+ from .geti_upload_node import GetiUploadNode
2
+
3
+ __all__ = ['GetiUploadNode']