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,345 @@
1
+ """
2
+ vLLM Node - Sends images and prompts to a vLLM OpenAI-compatible server.
3
+
4
+ Calls the /v1/chat/completions endpoint with vision-language model support.
5
+ All configuration fields (server_url, model, system_prompt, prompt, etc.)
6
+ can be set in the UI and overridden at runtime via incoming message fields.
7
+ """
8
+
9
+ import json
10
+ import base64
11
+ import threading
12
+ import logging
13
+ from typing import Optional, Dict, Any, List
14
+
15
+ try:
16
+ import requests
17
+ REQUESTS_AVAILABLE = True
18
+ except ImportError:
19
+ REQUESTS_AVAILABLE = False
20
+ import urllib.request
21
+ import urllib.error
22
+ import ssl
23
+
24
+ try:
25
+ from ..base_node import BaseNode, MessageKeys, Info
26
+ except ImportError:
27
+ from base_node import BaseNode, MessageKeys, Info
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Info panel
33
+ # ---------------------------------------------------------------------------
34
+ _info = Info()
35
+ _info.add_text(
36
+ "Sends images and text prompts to a vLLM server using the "
37
+ "OpenAI-compatible /v1/chat/completions endpoint."
38
+ )
39
+ _info.add_header("Inputs")
40
+ _info.add_bullets(
41
+ ("Input 0:", "Message with image data and/or prompt text"),
42
+ )
43
+ _info.add_header("Outputs")
44
+ _info.add_bullets(
45
+ ("Output 0:", "Model response"),
46
+ )
47
+ _info.add_bullets(
48
+ ("payload.content:", "The model's text response"),
49
+ ("payload.model:", "Model name used"),
50
+ ("payload.usage:", "Token usage statistics"),
51
+ ("payload.elapsed:", "Response time in seconds"),
52
+ )
53
+ _info.add_header("Configuration")
54
+ _info.add_text(
55
+ "All fields below can be set in the properties panel. "
56
+ "Any field can be overridden at runtime by the incoming message:"
57
+ )
58
+ _info.add_bullets(
59
+ ("msg.payload.prompt:", "Override the user prompt"),
60
+ ("msg.payload.system_prompt:", "Override the system prompt"),
61
+ ("msg.payload.image:", "Image data (base64 string or image dict)"),
62
+ ("msg.payload.server_url:", "Override the server URL"),
63
+ ("msg.payload.model:", "Override the model name"),
64
+ ("msg.payload.max_tokens:", "Override max tokens"),
65
+ )
66
+
67
+
68
+ class VLLMNode(BaseNode):
69
+ """
70
+ vLLM Node - Interface to vLLM OpenAI-compatible vision-language models.
71
+
72
+ Builds an OpenAI chat completions request with optional image input
73
+ and sends it to a vLLM server. Configuration can be set via the UI
74
+ properties panel and overridden at runtime through incoming message fields.
75
+ """
76
+
77
+ display_name = 'vLLM'
78
+ info = str(_info)
79
+ icon = '🤖'
80
+ category = 'vision'
81
+ color = '#7C3AED'
82
+ border_color = '#5B21B6'
83
+ text_color = '#FFFFFF'
84
+ input_count = 1
85
+ output_count = 1
86
+
87
+ DEFAULT_CONFIG: Dict[str, Any] = {
88
+ 'server_url': 'http://localhost:8000',
89
+ 'model': 'Qwen/Qwen3.5-9B',
90
+ 'system_prompt': 'You are a helpful assistant that analyzes images.',
91
+ 'prompt': 'Describe what you see in this image.',
92
+ 'max_tokens': 1024,
93
+ 'timeout': 60,
94
+ 'enable_thinking': False,
95
+ 'image_path': 'payload.image',
96
+ }
97
+
98
+ properties = [
99
+ {
100
+ 'name': 'server_url',
101
+ 'label': 'Server URL',
102
+ 'type': 'text',
103
+ 'placeholder': 'http://localhost:8000',
104
+ },
105
+ {
106
+ 'name': 'model',
107
+ 'label': 'Model',
108
+ 'type': 'text',
109
+ 'placeholder': 'Qwen/Qwen3.5-9B',
110
+ },
111
+ {
112
+ 'name': 'system_prompt',
113
+ 'label': 'System Prompt',
114
+ 'type': 'textarea',
115
+ 'placeholder': 'You are a helpful assistant.',
116
+ },
117
+ {
118
+ 'name': 'prompt',
119
+ 'label': 'User Prompt',
120
+ 'type': 'textarea',
121
+ 'placeholder': 'Describe what you see in this image.',
122
+ },
123
+ {
124
+ 'name': 'max_tokens',
125
+ 'label': 'Max Tokens',
126
+ 'type': 'text',
127
+ 'placeholder': '1024',
128
+ },
129
+ {
130
+ 'name': 'timeout',
131
+ 'label': 'Timeout (seconds)',
132
+ 'type': 'text',
133
+ 'placeholder': '60',
134
+ },
135
+ {
136
+ 'name': 'enable_thinking',
137
+ 'label': 'Enable Thinking',
138
+ 'type': 'checkbox',
139
+ 'default': False,
140
+ },
141
+ {
142
+ 'name': 'image_path',
143
+ 'label': 'Image Path in Message',
144
+ 'type': 'text',
145
+ 'placeholder': 'payload.image',
146
+ },
147
+ ]
148
+
149
+ def __init__(self, node_id: Optional[str] = None, name: str = "vllm"):
150
+ super().__init__(node_id, name)
151
+ self.configure(self.DEFAULT_CONFIG.copy())
152
+
153
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
154
+ """Handle incoming messages by calling vLLM in a background thread."""
155
+ thread = threading.Thread(
156
+ target=self._call_vllm,
157
+ args=(msg,),
158
+ daemon=True,
159
+ )
160
+ thread.start()
161
+
162
+ # ------------------------------------------------------------------
163
+ # Resolve a config value, allowing msg override
164
+ # ------------------------------------------------------------------
165
+ def _resolve(self, msg: Dict[str, Any], key: str, default: Any = None) -> Any:
166
+ """
167
+ Get a value from msg.payload.{key} first, falling back to config.
168
+ """
169
+ payload = msg.get(MessageKeys.PAYLOAD)
170
+ if isinstance(payload, dict) and key in payload:
171
+ return payload[key]
172
+ return self.config.get(key, default)
173
+
174
+ # ------------------------------------------------------------------
175
+ # Build the image data URL from various input formats
176
+ # ------------------------------------------------------------------
177
+ def _get_image_data_url(self, msg: Dict[str, Any]) -> Optional[str]:
178
+ """
179
+ Extract image from the message and return as a data URL string.
180
+ Supports: base64 string, dict with data/encoding/format, numpy array.
181
+ """
182
+ image_path = self.config.get('image_path', 'payload.image')
183
+ image_data = self._get_nested_value(msg, image_path)
184
+
185
+ # Also check msg.payload.image as direct override
186
+ payload = msg.get(MessageKeys.PAYLOAD)
187
+ if image_data is None and isinstance(payload, dict):
188
+ image_data = payload.get('image')
189
+
190
+ if image_data is None:
191
+ return None
192
+
193
+ # Already a data URL
194
+ if isinstance(image_data, str) and image_data.startswith('data:image'):
195
+ return image_data
196
+
197
+ # Raw base64 string
198
+ if isinstance(image_data, str):
199
+ return f'data:image/jpeg;base64,{image_data}'
200
+
201
+ # Camera node dict format: {'format': 'jpeg', 'encoding': 'base64', 'data': '...'}
202
+ if isinstance(image_data, dict):
203
+ data = image_data.get('data')
204
+ fmt = image_data.get('format', 'jpeg')
205
+ encoding = image_data.get('encoding', '')
206
+
207
+ if encoding == 'base64' and isinstance(data, str):
208
+ return f'data:image/{fmt};base64,{data}'
209
+
210
+ # numpy array inside dict — encode to JPEG base64
211
+ try:
212
+ import numpy as np
213
+ import cv2
214
+ if isinstance(data, np.ndarray):
215
+ ret, buf = cv2.imencode('.jpg', data)
216
+ if ret:
217
+ b64 = base64.b64encode(buf.tobytes()).decode('utf-8')
218
+ return f'data:image/jpeg;base64,{b64}'
219
+ except ImportError:
220
+ pass
221
+
222
+ # Direct numpy array
223
+ try:
224
+ import numpy as np
225
+ import cv2
226
+ if isinstance(image_data, np.ndarray):
227
+ ret, buf = cv2.imencode('.jpg', image_data)
228
+ if ret:
229
+ b64 = base64.b64encode(buf.tobytes()).decode('utf-8')
230
+ return f'data:image/jpeg;base64,{b64}'
231
+ except ImportError:
232
+ pass
233
+
234
+ self.report_error(f"Unsupported image format: {type(image_data).__name__}")
235
+ return None
236
+
237
+ # ------------------------------------------------------------------
238
+ # Main request logic
239
+ # ------------------------------------------------------------------
240
+ def _call_vllm(self, msg: Dict[str, Any]):
241
+ """Build the payload and call the vLLM chat completions endpoint."""
242
+ import time as _time
243
+
244
+ server_url = self._resolve(msg, 'server_url', self.DEFAULT_CONFIG['server_url'])
245
+ model = self._resolve(msg, 'model', self.DEFAULT_CONFIG['model'])
246
+ system_prompt = self._resolve(msg, 'system_prompt', self.DEFAULT_CONFIG['system_prompt'])
247
+ prompt = self._resolve(msg, 'prompt', self.DEFAULT_CONFIG['prompt'])
248
+ max_tokens = int(self._resolve(msg, 'max_tokens', self.DEFAULT_CONFIG['max_tokens']))
249
+ timeout = int(self._resolve(msg, 'timeout', self.DEFAULT_CONFIG['timeout']))
250
+ enable_thinking = self._resolve(msg, 'enable_thinking', self.DEFAULT_CONFIG['enable_thinking'])
251
+ if isinstance(enable_thinking, str):
252
+ enable_thinking = enable_thinking.lower() in ('true', '1', 'yes')
253
+
254
+ # Build user content (text only, or text + image)
255
+ user_content: Any
256
+ image_url = self._get_image_data_url(msg)
257
+ if image_url:
258
+ user_content = [
259
+ {"type": "image_url", "image_url": {"url": image_url}},
260
+ {"type": "text", "text": prompt},
261
+ ]
262
+ else:
263
+ user_content = prompt
264
+
265
+ # Build messages
266
+ messages = []
267
+ if system_prompt:
268
+ messages.append({"role": "system", "content": system_prompt})
269
+ messages.append({"role": "user", "content": user_content})
270
+
271
+ # Build request payload
272
+ request_payload: Dict[str, Any] = {
273
+ "model": model,
274
+ "messages": messages,
275
+ "max_tokens": max_tokens,
276
+ }
277
+
278
+ request_payload["chat_template_kwargs"] = {"enable_thinking": enable_thinking}
279
+
280
+ url = f"{server_url.rstrip('/')}/v1/chat/completions"
281
+ start = _time.time()
282
+
283
+ try:
284
+ response_data = self._post_json(url, request_payload, timeout)
285
+ elapsed = _time.time() - start
286
+
287
+ # Extract content from response
288
+ content = ''
289
+ usage = {}
290
+ if isinstance(response_data, dict):
291
+ choices = response_data.get('choices', [])
292
+ if choices:
293
+ content = choices[0].get('message', {}).get('content', '')
294
+ usage = response_data.get('usage', {})
295
+
296
+ out_msg = self.create_message(
297
+ payload={
298
+ 'content': content,
299
+ 'model': model,
300
+ 'usage': usage,
301
+ 'elapsed': round(elapsed, 3),
302
+ },
303
+ topic=msg.get(MessageKeys.TOPIC, ''),
304
+ )
305
+ # Preserve non-internal fields from original message
306
+ for key in msg:
307
+ if key not in out_msg and key != MessageKeys.PAYLOAD and not key.startswith('_'):
308
+ out_msg[key] = msg[key]
309
+
310
+ self.send(out_msg)
311
+
312
+ except Exception as exc:
313
+ elapsed = _time.time() - start
314
+ logger.error("VLLMNode [%s] request failed: %s", self.name, exc)
315
+ self.report_error(f"vLLM request failed: {exc}")
316
+
317
+ out_msg = self.create_message(
318
+ payload={
319
+ 'content': None,
320
+ 'error': str(exc),
321
+ 'model': model,
322
+ 'elapsed': round(elapsed, 3),
323
+ },
324
+ topic=f"{msg.get(MessageKeys.TOPIC, '')}/error",
325
+ )
326
+ self.send(out_msg)
327
+
328
+ # ------------------------------------------------------------------
329
+ # HTTP helpers
330
+ # ------------------------------------------------------------------
331
+ def _post_json(self, url: str, payload: Dict[str, Any], timeout: int) -> Any:
332
+ """POST JSON and return parsed response. Uses requests if available."""
333
+ data = json.dumps(payload).encode('utf-8')
334
+ headers = {'Content-Type': 'application/json'}
335
+
336
+ if REQUESTS_AVAILABLE:
337
+ resp = requests.post(url, json=payload, timeout=timeout)
338
+ resp.raise_for_status()
339
+ return resp.json()
340
+
341
+ # Fallback to urllib
342
+ req = urllib.request.Request(url, data=data, headers=headers, method='POST')
343
+ ctx = ssl.create_default_context()
344
+ with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
345
+ return json.loads(resp.read().decode('utf-8'))
@@ -0,0 +1,3 @@
1
+ from .video_reader_node import VideoReaderNode
2
+
3
+ __all__ = ['VideoReaderNode']