pynode-flow 0.2.5__tar.gz → 0.2.7__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 (349) hide show
  1. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/.claude/skills/pynode-workflows/SKILL.md +26 -4
  2. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/INSTALL.md +1 -1
  3. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/PKG-INFO +3 -2
  4. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/README.md +2 -1
  5. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/_version.py +3 -3
  6. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/api/sse.py +6 -1
  7. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/node_registry.py +1 -0
  8. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/DebugNode/debug_node.py +13 -2
  9. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/ImageUploadNode/image_upload_node.py +21 -0
  10. pynode_flow-0.2.7/pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +314 -0
  11. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +13 -7
  12. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/InferenceNode/inference_node.py +28 -9
  13. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +13 -0
  14. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SliceNode/slice_collector_node.py +247 -9
  15. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SliceNode/slice_image_node.py +124 -15
  16. pynode_flow-0.2.7/pynode/nodes/Supervision/__init__.py +25 -0
  17. pynode_flow-0.2.7/pynode/nodes/Supervision/requirements.txt +3 -0
  18. pynode_flow-0.2.7/pynode/nodes/Supervision/supervision_annotate_node.py +393 -0
  19. pynode_flow-0.2.7/pynode/nodes/Supervision/supervision_filter_node.py +278 -0
  20. pynode_flow-0.2.7/pynode/nodes/Supervision/supervision_line_counter_node.py +313 -0
  21. pynode_flow-0.2.7/pynode/nodes/Supervision/supervision_sink_node.py +223 -0
  22. pynode_flow-0.2.7/pynode/nodes/Supervision/supervision_smoother_node.py +146 -0
  23. pynode_flow-0.2.7/pynode/nodes/Supervision/supervision_tracker_node.py +229 -0
  24. pynode_flow-0.2.7/pynode/nodes/Supervision/supervision_zone_node.py +316 -0
  25. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/UltralyticsNode/ultralytics_node.py +18 -7
  26. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/VideoReaderNode/video_reader_node.py +8 -2
  27. pynode_flow-0.2.7/pynode/nodes/supervision_utils.py +375 -0
  28. pynode_flow-0.2.7/pynode/static/examples/18-track-trace.json +87 -0
  29. pynode_flow-0.2.7/pynode/static/examples/19-line-counter.json +110 -0
  30. pynode_flow-0.2.7/pynode/static/examples/20-zone-occupancy.json +112 -0
  31. pynode_flow-0.2.7/pynode/static/examples/21-annotator-gallery.json +170 -0
  32. pynode_flow-0.2.7/pynode/static/examples/22-heatmap.json +113 -0
  33. pynode_flow-0.2.7/pynode/static/examples/23-privacy-blur.json +95 -0
  34. pynode_flow-0.2.7/pynode/static/examples/24-detection-log.json +108 -0
  35. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/examples/README.md +29 -1
  36. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/examples/manifest.json +49 -0
  37. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/js/config.js +1 -0
  38. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/js/debug.js +13 -0
  39. pynode_flow-0.2.7/pynode/static/js/geometry-editor.js +236 -0
  40. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/js/main.js +2 -0
  41. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/js/nodes.js +71 -22
  42. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/js/properties.js +30 -1
  43. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/style.css +103 -0
  44. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode_flow.egg-info/PKG-INFO +3 -2
  45. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode_flow.egg-info/SOURCES.txt +24 -3
  46. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode_flow.egg-info/scm_file_list.json +296 -275
  47. pynode_flow-0.2.7/pynode_flow.egg-info/scm_version.json +8 -0
  48. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/setup.bat +55 -27
  49. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/setup.sh +38 -15
  50. pynode_flow-0.2.7/tests/test_debug_json_safety.py +89 -0
  51. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_device_detection.py +146 -1
  52. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_image_upload_node.py +28 -0
  53. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_node_types_api.py +1 -0
  54. pynode_flow-0.2.7/tests/test_supervision_annotate_node.py +259 -0
  55. pynode_flow-0.2.7/tests/test_supervision_phase3_nodes.py +293 -0
  56. pynode_flow-0.2.7/tests/test_supervision_tracker_node.py +226 -0
  57. pynode_flow-0.2.7/tests/test_supervision_utils.py +180 -0
  58. pynode_flow-0.2.7/tests/test_supervision_zone_nodes.py +323 -0
  59. pynode_flow-0.2.5/pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +0 -149
  60. pynode_flow-0.2.5/pynode/nodes/TrackerNode/__init__.py +0 -2
  61. pynode_flow-0.2.5/pynode/nodes/TrackerNode/supervision_tracker_node.py +0 -251
  62. pynode_flow-0.2.5/pynode/nodes/TrackerNode/tracker_node.py +0 -745
  63. pynode_flow-0.2.5/pynode_flow.egg-info/scm_version.json +0 -8
  64. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/.dockerignore +0 -0
  65. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/.github/workflows/ci.yml +0 -0
  66. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/.github/workflows/publish.yaml +0 -0
  67. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/.gitignore +0 -0
  68. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/DOCKER.md +0 -0
  69. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/Dockerfile +0 -0
  70. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/Dockerfile.cpu +0 -0
  71. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/LICENSE +0 -0
  72. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/MANIFEST.in +0 -0
  73. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/docker-compose.yml +0 -0
  74. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/docker-entrypoint.sh +0 -0
  75. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/docs/CREATING_NODES.md +0 -0
  76. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/docs/CUSTOM_NODES.md +0 -0
  77. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/docs/DOCKER.md +0 -0
  78. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/docs/EXTENSIBILITY.md +0 -0
  79. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/docs/UI_COMPONENTS.md +0 -0
  80. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/examples/README.md +0 -0
  81. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/examples/camera_workflow.py +0 -0
  82. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/examples/camera_yolo_workflow.py +0 -0
  83. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/install_nodes.bat +0 -0
  84. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/install_nodes.sh +0 -0
  85. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/__init__.py +0 -0
  86. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/__main__.py +0 -0
  87. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/_perf.py +0 -0
  88. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/api/__init__.py +0 -0
  89. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/api/helpers.py +0 -0
  90. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/api/nodes.py +0 -0
  91. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/api/services.py +0 -0
  92. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/api/uploads.py +0 -0
  93. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/api/workflows.py +0 -0
  94. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/config.py +0 -0
  95. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/install_nodes.py +0 -0
  96. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/main.py +0 -0
  97. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/AutoSyncNode/auto_sync_node.py +0 -0
  98. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/BBoxMetricsNode/__init__.py +0 -0
  99. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +0 -0
  100. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/BBoxMetricsNode/requirements.txt +0 -0
  101. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/BatchNode/__init__.py +0 -0
  102. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/BatchNode/batch_node.py +0 -0
  103. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/BatchNode/requirements.txt +0 -0
  104. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/CameraNode/__init__.py +0 -0
  105. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/CameraNode/camera_node.py +0 -0
  106. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/CameraNode/requirements.txt +0 -0
  107. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/ChangeNode/__init__.py +0 -0
  108. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/ChangeNode/change_node.py +0 -0
  109. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/ChangeNode/requirements.txt +0 -0
  110. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/ConfidenceFilterNode/__init__.py +0 -0
  111. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +0 -0
  112. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/ConfidenceFilterNode/requirements.txt +0 -0
  113. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/ControlSliderNode/control_slider_node.py +0 -0
  114. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/CounterNode/__init__.py +0 -0
  115. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/CounterNode/counter_node.py +0 -0
  116. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/CropNode/__init__.py +0 -0
  117. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/CropNode/crop_node.py +0 -0
  118. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/CropNode/requirements.txt +0 -0
  119. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/DebugNode/__init__.py +0 -0
  120. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/DebugNode/requirements.txt +0 -0
  121. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/DelayNode/delay_node.py +0 -0
  122. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/DelayNode/requirements.txt +0 -0
  123. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/DrawPredictionsNode/__init__.py +0 -0
  124. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +0 -0
  125. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/DrawPredictionsNode/requirements.txt +0 -0
  126. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/ErrorNode/error_node.py +0 -0
  127. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/ErrorNode/requirements.txt +0 -0
  128. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/FilterNode/__init__.py +0 -0
  129. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/FilterNode/filter_node.py +0 -0
  130. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/FilterNode/requirements.txt +0 -0
  131. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/FrameSourceNode/__init__.py +0 -0
  132. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/FrameSourceNode/frame_source_node.py +0 -0
  133. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/FrameSourceNode/requirements.txt +0 -0
  134. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/FunctionNode/__init__.py +0 -0
  135. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/FunctionNode/function_node.py +0 -0
  136. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/FunctionNode/requirements.txt +0 -0
  137. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/GateNode/gate_node.py +0 -0
  138. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/GateNode/requirements.txt +0 -0
  139. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/ImageFormatNode/__init__.py +0 -0
  140. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/ImageFormatNode/image_format_node.py +0 -0
  141. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/ImageFormatNode/requirements.txt +0 -0
  142. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/ImageViewerNode/__init__.py +0 -0
  143. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/ImageViewerNode/image_viewer_node.py +0 -0
  144. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/ImageViewerNode/requirements.txt +0 -0
  145. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/ImageWriterNode/__init__.py +0 -0
  146. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/ImageWriterNode/imagewriter_node.py +0 -0
  147. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/ImageWriterNode/requirements.txt +0 -0
  148. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/InferenceNode/InferenceEngine/__init__.py +0 -0
  149. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +0 -0
  150. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +0 -0
  151. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +0 -0
  152. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +0 -0
  153. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +0 -0
  154. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/InferenceNode/InferenceEngine/readme.md +0 -0
  155. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +0 -0
  156. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/InferenceNode/requirements.txt +0 -0
  157. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/InjectNode/inject_node.py +0 -0
  158. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/InjectNode/requirements.txt +0 -0
  159. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/JoinNode/__init__.py +0 -0
  160. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/JoinNode/join_node.py +0 -0
  161. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/JoinNode/requirements.txt +0 -0
  162. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/LabelFilterNode/__init__.py +0 -0
  163. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/LabelFilterNode/label_filter_node.py +0 -0
  164. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/LabelFilterNode/requirements.txt +0 -0
  165. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/LinkNode/__init__.py +0 -0
  166. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/LinkNode/link_bus.py +0 -0
  167. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/LinkNode/link_in_node.py +0 -0
  168. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/LinkNode/link_out_node.py +0 -0
  169. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/LinkNode/requirements.txt +0 -0
  170. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/MQTTNode/__init__.py +0 -0
  171. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/MQTTNode/mqtt_in_node.py +0 -0
  172. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/MQTTNode/mqtt_out_node.py +0 -0
  173. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/MQTTNode/mqtt_service.py +0 -0
  174. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/MQTTNode/requirements.txt +0 -0
  175. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/MessageWriterNode/__init__.py +0 -0
  176. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/MessageWriterNode/messagereader_node.py +0 -0
  177. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/MessageWriterNode/messagewriter_node.py +0 -0
  178. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OmronCameraNode/__init__.py +0 -0
  179. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OmronCameraNode/omron_camera_node.py +0 -0
  180. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OmronCameraNode/requirements.txt +0 -0
  181. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/__init__.py +0 -0
  182. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/arithmetic_node.py +0 -0
  183. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/bitwise_node.py +0 -0
  184. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/blend_node.py +0 -0
  185. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/blob_detector_node.py +0 -0
  186. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/blur_node.py +0 -0
  187. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/color_space_node.py +0 -0
  188. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/colormap_node.py +0 -0
  189. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/contours_node.py +0 -0
  190. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/denormalize_coords_node.py +0 -0
  191. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/draw_node.py +0 -0
  192. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/edge_detector_node.py +0 -0
  193. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/equirectangular_node.py +0 -0
  194. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/fft_node.py +0 -0
  195. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/focus_estimation_node.py +0 -0
  196. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/frequency_filter_node.py +0 -0
  197. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/histogram_node.py +0 -0
  198. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/hough_circles_node.py +0 -0
  199. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/hough_lines_node.py +0 -0
  200. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/in_range_node.py +0 -0
  201. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/morphology_node.py +0 -0
  202. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/normalize_coords_node.py +0 -0
  203. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/paste_node.py +0 -0
  204. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/perspective_node.py +0 -0
  205. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/realsense_depth_node.py +0 -0
  206. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/requirements.txt +0 -0
  207. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/resize_node.py +0 -0
  208. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/rotate_node.py +0 -0
  209. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/template_match_node.py +0 -0
  210. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/threshold_node.py +0 -0
  211. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/PointInShapeNode/__init__.py +0 -0
  212. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/PointInShapeNode/point_in_shape_node.py +0 -0
  213. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/PointInShapeNode/requirements.txt +0 -0
  214. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/PolygonMetricsNode/__init__.py +0 -0
  215. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +0 -0
  216. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/PolygonMetricsNode/requirements.txt +0 -0
  217. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/QueueLengthProbeNode/__init__.py +0 -0
  218. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +0 -0
  219. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/Qwen3VLMNode/__init__.py +0 -0
  220. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/Qwen3VLMNode/requirements.txt +0 -0
  221. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/RESTEndpointNode/__init__.py +0 -0
  222. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +0 -0
  223. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/RangeNode/__init__.py +0 -0
  224. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/RangeNode/range_node.py +0 -0
  225. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/RangeNode/requirements.txt +0 -0
  226. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/RateProbeNode/__init__.py +0 -0
  227. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/RateProbeNode/rate_probe_node.py +0 -0
  228. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/RateProbeNode/requirements.txt +0 -0
  229. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/RoboflowUploadNode/__init__.py +0 -0
  230. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/RoboflowUploadNode/requirements.txt +0 -0
  231. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +0 -0
  232. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SliceNode/README.md +0 -0
  233. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SliceNode/__init__.py +0 -0
  234. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SliceNode/merge_predictions_node.py +0 -0
  235. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SliceNode/requirements.txt +0 -0
  236. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/__init__.py +0 -0
  237. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/interop/README.md +0 -0
  238. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/interop/nodered-example-flow.json +0 -0
  239. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/ndjson_protocol.py +0 -0
  240. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/nodered_snippets.py +0 -0
  241. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/requirements.txt +0 -0
  242. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/tcp_in_node.py +0 -0
  243. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/tcp_out_node.py +0 -0
  244. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/udp_in_node.py +0 -0
  245. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/udp_out_node.py +0 -0
  246. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/udp_probe.py +0 -0
  247. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/udp_protocol.py +0 -0
  248. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SplitNode/__init__.py +0 -0
  249. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SplitNode/requirements.txt +0 -0
  250. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SplitNode/split_node.py +0 -0
  251. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SwitchNode/requirements.txt +0 -0
  252. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SwitchNode/switch_node.py +0 -0
  253. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SyncNode/__init__.py +0 -0
  254. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/SyncNode/sync_node.py +0 -0
  255. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/TemplateNode/requirements.txt +0 -0
  256. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/TemplateNode/template_node.py +0 -0
  257. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/UltralyticsNode/__init__.py +0 -0
  258. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/UltralyticsNode/model_paths.py +0 -0
  259. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/UltralyticsNode/requirements.txt +0 -0
  260. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/UnknownNode/__init__.py +0 -0
  261. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/VLLMNode/__init__.py +0 -0
  262. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/VLLMNode/vllm_node.py +0 -0
  263. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/VideoReaderNode/__init__.py +0 -0
  264. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/VideoWriterNode/__init__.py +0 -0
  265. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/VideoWriterNode/video_writer_node.py +0 -0
  266. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/WebhookNode/__init__.py +0 -0
  267. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/WebhookNode/webhook_node.py +0 -0
  268. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/__init__.py +0 -0
  269. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/base_node.py +0 -0
  270. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/image_utils.py +0 -0
  271. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/info.py +0 -0
  272. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/mDNSNode/__init__.py +0 -0
  273. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/mDNSNode/mdns_broadcast_node.py +0 -0
  274. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/mDNSNode/mdns_discovery_node.py +0 -0
  275. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/mDNSNode/mdns_manager.py +0 -0
  276. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/mDNSNode/requirements.txt +0 -0
  277. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/messages.py +0 -0
  278. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/nodes/pacing.py +0 -0
  279. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/server.py +0 -0
  280. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/examples/01-hello-world.json +0 -0
  281. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/examples/02-function-transform.json +0 -0
  282. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/examples/03-switch-routing.json +0 -0
  283. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/examples/04-flow-control.json +0 -0
  284. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/examples/05-webcam-viewer.json +0 -0
  285. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/examples/06-webcam-yolo.json +0 -0
  286. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/examples/07-blur-people.json +0 -0
  287. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/examples/08-video-yolo-recorder.json +0 -0
  288. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/examples/09-mqtt-pub-sub.json +0 -0
  289. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/examples/10-person-alert-mqtt.json +0 -0
  290. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/examples/16-socket-bridge.json +0 -0
  291. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/examples/17-slider-crop.json +0 -0
  292. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/images/favicon4.png +0 -0
  293. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/index.html +0 -0
  294. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/js/README.md +0 -0
  295. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/js/auth.js +0 -0
  296. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/js/clipboard.js +0 -0
  297. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/js/connections.js +0 -0
  298. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/js/events.js +0 -0
  299. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/js/history.js +0 -0
  300. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/js/minimap.js +0 -0
  301. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/js/mqtt-services.js +0 -0
  302. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/js/palette.js +0 -0
  303. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/js/selection.js +0 -0
  304. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/js/state.js +0 -0
  305. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/js/ui-utils.js +0 -0
  306. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/js/viewport.js +0 -0
  307. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/js/workflow.js +0 -0
  308. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/static/js/workflows.js +0 -0
  309. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/workflow_engine.py +0 -0
  310. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode/workflow_manager.py +0 -0
  311. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode_flow.egg-info/dependency_links.txt +0 -0
  312. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode_flow.egg-info/entry_points.txt +0 -0
  313. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode_flow.egg-info/requires.txt +0 -0
  314. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pynode_flow.egg-info/top_level.txt +0 -0
  315. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/pyproject.toml +0 -0
  316. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/requirements.txt +0 -0
  317. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/setup.cfg +0 -0
  318. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/setup.py +0 -0
  319. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/__init__.py +0 -0
  320. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/conftest.py +0 -0
  321. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_api_auth.py +0 -0
  322. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_api_errors.py +0 -0
  323. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_api_nodes.py +0 -0
  324. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_api_security.py +0 -0
  325. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_api_workflows.py +0 -0
  326. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_base_node_messaging.py +0 -0
  327. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_camera_node_backend.py +0 -0
  328. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_concurrency.py +0 -0
  329. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_config.py +0 -0
  330. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_control_slider_node.py +0 -0
  331. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_counter_sse.py +0 -0
  332. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_crop_node.py +0 -0
  333. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_debug_workflow_scope.py +0 -0
  334. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_delay_rate_limit.py +0 -0
  335. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_frame_pacing.py +0 -0
  336. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_function_node.py +0 -0
  337. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_link_nodes.py +0 -0
  338. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_message_reader_pickle.py +0 -0
  339. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_models_dir.py +0 -0
  340. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_mqtt_services.py +0 -0
  341. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_node_registration.py +0 -0
  342. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_persistence.py +0 -0
  343. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_range_node.py +0 -0
  344. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_send_copy_semantics.py +0 -0
  345. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_serialization.py +0 -0
  346. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_socket_nodes.py +0 -0
  347. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_video_reader_node.py +0 -0
  348. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_workflow_engine.py +0 -0
  349. {pynode_flow-0.2.5 → pynode_flow-0.2.7}/tests/test_workflow_roundtrip.py +0 -0
@@ -84,11 +84,30 @@ Messages are dicts in Node-RED style: `payload`, `topic`, `_msgid`, plus extras.
84
84
  | `ImageViewerNode` | 1→0 | `width`: 320; `height`: 240; `image_path`: "payload.image"; `drop_messages`: true |
85
85
  | `VideoWriterNode` | 1→0 | `path`: "./output"; `filename`: "video_{counter}"; `codec`: `mp4v`\|`avc1`\|`xvid`...; `framerate`: 30.0; `width`/`height`; `auto_resolution`: bool (match input); `clip_length`: 0 (=unlimited); `naming_mode`: "counter"; `counter_digits`: 4 |
86
86
 
87
- Other vision nodes: `DrawPredictionsNode`, `CropNode`, `TrackerNode`,
87
+ Other vision nodes: `DrawPredictionsNode`, `CropNode`,
88
88
  `ImageFormatNode`, `SliceNode`, `BBoxMetricsNode`, `PolygonMetricsNode`,
89
89
  `InferenceNode` (multi-backend), `FrameSourceNode` (multi-source camera lib) —
90
90
  read their `DEFAULT_CONFIG`/`properties` in `pynode/nodes/<Name>/` before use.
91
91
 
92
+ ### Supervision
93
+ Nodes built on the `supervision` library (`[vision]` extra). They register even
94
+ without it installed, reporting an error and passing messages through.
95
+
96
+ | Type | Ports | Config |
97
+ |---|---|---|
98
+ | `SupervisionTrackerNode` | 1→1 | `track_thresh`: "0.25"; `track_buffer`: "30" (frames a lost track is kept, scaled by `frame_rate`); `match_thresh`: "0.8" (IoU); `min_consecutive_frames`: "1" (raise to 2-3 to kill ghost tracks); `frame_rate`: "30" (**set to the real source fps**). Adds `track_id` to each detection plus `payload.tracks`/`track_count`. Has a `reset` action — a new object needs 2 frames before it is reported. **Does not draw** — wire an `SupervisionAnnotateNode` after it. |
99
+ | `SupervisionAnnotateNode` | 1→1 | `annotators`: list from `box round_box box_corner ellipse circle triangle dot color label percentage_bar blur pixelate background_overlay trace heatmap` (applied in a fixed order: pixel effects → shapes → labels; default `["box","label"]`); `color_lookup`: `class`\|`track`\|`index` (`track` falls back to `class` without ids); `thickness`/`text_scale`: "auto" (resolution-scaled) or a number; `show_class`/`show_confidence`/`show_track_id`: bool (label content); `trace_length`: "30". `trace` needs a `SupervisionTrackerNode` upstream; `heatmap` accumulates over time. `reset` action clears trace/heatmap state. |
100
+ | `SupervisionLineCounterNode` | 1→**2** | `line`: "x1,y1,x2,y2" (image px; the GUI has a draw-on-frame editor); `anchors`: `corners`\|`center`\|`bottom_center`; `min_crossing_frames`: "1"; `draw`: "true"/"false". **Needs tracked detections** (SV Tracker upstream). Output 0 = every msg + `payload.line_in`/`line_out`/`line_in_by_class`/`line_out_by_class` and the line drawn; **output 1 = one msg per crossing** `{event:'line_crossing', direction, track_id, class_id, class_name, bbox, in_count, out_count}`. Actions: `reset` (zero counts), `set_geometry` (live line update, resets counts). |
101
+ | `SupervisionZoneNode` | 1→**2** | `polygon`: JSON "[[x,y],...]" (image px; draw-on-frame editor in GUI); `anchor`: `bottom_center`\|`center`\|`top_center`; `filter_to_zone`: bool (drop outside detections); `draw`: "true"/"false"; `opacity`: "0.15". Output 0 = every msg + `payload.zone_count` and `in_zone` on each detection; **output 1 = enter/exit events** (needs track ids) `{event:'enter'\|'exit', track_id, class_id, class_name, bbox, zone_count}`. Actions: `reset` (event state), `set_geometry` (live polygon update). |
102
+ | `SupervisionSmootherNode` | 1→1 | `length`: "5" (frames averaged per box). Smooths bbox jitter; **needs track ids** (SV Tracker upstream) — place between tracker and annotate/zones. Untracked input passes through with one warning. `reset` action clears history. |
103
+ | `SupervisionFilterNode` | 1→**2** | All criteria optional (empty = off): `min_confidence`/`max_confidence`; `allow_classes`/`deny_classes` (comma names or numeric ids); `min_area`/`max_area` (px²); `min_aspect`/`max_aspect` (w/h); `top_k`; `dedupe`: `off`\|`nms`\|`nmm` + `dedupe_threshold`: "0.5" + `overlap_metric`: `iou`\|`ios` + `class_agnostic`: bool. Output 0 = kept, output 1 = rejected by criteria (both always fire; NMS/NMM-suppressed are dropped, not rejected). |
104
+ | `SupervisionSinkNode` | 1→1 | `format`: `csv` (row per detection, safe for long runs)\|`json` (in-memory, written on stop/rotate); `path`: "./output"; `filename`: base → `<base>_<YYYYmmdd-HHMMSS>.<ext>`. Row = xyxy, class_id, confidence, tracker_id, class_name, frame, timestamp. Empty frames advance the frame counter but write no rows; the file opens on the first detection. Passes msgs through with `payload.sink_file`/`sink_rows`. `new_file` action rotates. |
105
+
106
+ Detections travel as `payload.detections` (list of dicts, read by every core
107
+ node) and, alongside it, `payload.sv` — the live `sv.Detections` object, which
108
+ also carries masks and tracker ids. Use `pynode/nodes/supervision_utils.py`
109
+ (`to_sv` / `write_back`) rather than converting by hand.
110
+
92
111
  ### Network
93
112
  | Type | Ports | Config |
94
113
  |---|---|---|
@@ -164,12 +183,15 @@ app = create_app({'WORKFLOWS_DIR': os.path.join(td, 'wf'),
164
183
  'WORKFLOW_FILE': os.path.join(td, 'wf', 'workflow.json'),
165
184
  'UPLOAD_BASE_DIR': os.path.join(td, 'up'), 'TESTING': True})
166
185
  client = app.test_client()
167
- data = json.load(open('pynode/static/examples/01-hello-world.json'))
168
- r = client.post('/api/workflow', json=data)
186
+ # A fresh app has NO workflows - create one first, then import into it.
187
+ r = client.post('/api/workflows', json={'name': 'validate'})
188
+ wf_id = r.get_json()['id']
189
+ data = json.load(open('pynode/static/examples/01-hello-world.json', encoding='utf-8'))
190
+ r = client.post(f'/api/workflow?workflow={wf_id}', json=data)
169
191
  assert r.status_code == 201, r.get_json()
170
192
  manager = app.extensions['workflow_manager']
171
193
  with manager.state_lock:
172
- eng = next(iter(manager.working_engines.values()))
194
+ eng = manager.working_engines[wf_id]
173
195
  unknown = [n.type for n in eng.nodes.values() if getattr(n, '_is_unknown_node', False)]
174
196
  assert not unknown, f"unknown node types: {unknown}"
175
197
  manager.shutdown()
@@ -32,7 +32,7 @@ Install only the node groups you need:
32
32
 
33
33
  | Extra | Installs | Nodes it enables |
34
34
  |-------|----------|------------------|
35
- | `vision` | ultralytics, torch, torchvision, supervision | UltralyticsNode, TrackerNode, DrawPredictionsNode, … |
35
+ | `vision` | ultralytics, torch, torchvision, supervision | UltralyticsNode, DrawPredictionsNode, the Supervision nodes, … |
36
36
  | `mqtt` | paho-mqtt | MQTTNode |
37
37
  | `camera` | framesource[full] | FrameSourceNode |
38
38
  | `inference` | onnxruntime (+ `vision`) | InferenceNode |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pynode-flow
3
- Version: 0.2.5
3
+ Version: 0.2.7
4
4
  Summary: Python based Node-RED-like visual workflow system
5
5
  Author: olkham
6
6
  License-Expression: MIT
@@ -94,7 +94,8 @@ carries its own documentation in the editor's **Information** panel (ℹ️ tab)
94
94
  | **Output** | Image Viewer, Image Writer, Video Writer, Message Writer, Roboflow Upload |
95
95
  | **Logic** | Switch (route on conditions), Gate, Sync, Auto Sync |
96
96
  | **Network** | MQTT In / Out, UDP In / Out, TCP In / Out, REST Endpoint, Webhook, mDNS Broadcast / Discovery |
97
- | **Vision** | YOLO, Inference, Tracker, Supervision Tracker, Crop, Slice Image / Slice Collector, Draw Predictions, Confidence Filter, Label Filter, Merge Predictions, Image Format, Qwen3-VL, vLLM |
97
+ | **Vision** | YOLO, Inference, Crop, Slice Image / Slice Collector, Draw Predictions, Confidence Filter, Label Filter, Merge Predictions, Image Format, Qwen3-VL, vLLM |
98
+ | **Supervision** | SV Tracker (ByteTrack), SV Annotate (15 visual styles: boxes, labels, traces, heat maps, privacy blur/pixelate), SV Line Counter (in/out crossing counts + events), SV Polygon Zone (occupancy, enter/exit events) — zone geometry drawn directly on a live frame — SV Smoother (de-jitter boxes), SV Detection Filter (confidence/class/area/aspect/top-K + NMS/NMM), SV Sink (CSV/JSON detection logging) |
98
99
  | **Analysis** | BBox Metrics, Polygon Metrics, Point in Shape |
99
100
  | **OpenCV** | ~28 image-processing nodes: Blur, Threshold, Morphology, Edge Detector, Find Contours, Perspective, Resize, Rotate, Colormap, Histogram, FFT, Template Match, RealSense Depth, and more |
100
101
  | **Node Probes** | Rate Probe, Queue Length Probe, Counter |
@@ -29,7 +29,8 @@ carries its own documentation in the editor's **Information** panel (ℹ️ tab)
29
29
  | **Output** | Image Viewer, Image Writer, Video Writer, Message Writer, Roboflow Upload |
30
30
  | **Logic** | Switch (route on conditions), Gate, Sync, Auto Sync |
31
31
  | **Network** | MQTT In / Out, UDP In / Out, TCP In / Out, REST Endpoint, Webhook, mDNS Broadcast / Discovery |
32
- | **Vision** | YOLO, Inference, Tracker, Supervision Tracker, Crop, Slice Image / Slice Collector, Draw Predictions, Confidence Filter, Label Filter, Merge Predictions, Image Format, Qwen3-VL, vLLM |
32
+ | **Vision** | YOLO, Inference, Crop, Slice Image / Slice Collector, Draw Predictions, Confidence Filter, Label Filter, Merge Predictions, Image Format, Qwen3-VL, vLLM |
33
+ | **Supervision** | SV Tracker (ByteTrack), SV Annotate (15 visual styles: boxes, labels, traces, heat maps, privacy blur/pixelate), SV Line Counter (in/out crossing counts + events), SV Polygon Zone (occupancy, enter/exit events) — zone geometry drawn directly on a live frame — SV Smoother (de-jitter boxes), SV Detection Filter (confidence/class/area/aspect/top-K + NMS/NMM), SV Sink (CSV/JSON detection logging) |
33
34
  | **Analysis** | BBox Metrics, Polygon Metrics, Point in Shape |
34
35
  | **OpenCV** | ~28 image-processing nodes: Blur, Threshold, Morphology, Edge Detector, Find Contours, Perspective, Resize, Rotate, Colormap, Histogram, FFT, Template Match, RealSense Depth, and more |
35
36
  | **Node Probes** | Rate Probe, Queue Length Probe, Counter |
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.2.5'
22
- __version_tuple__ = version_tuple = (0, 2, 5)
21
+ __version__ = version = '0.2.7'
22
+ __version_tuple__ = version_tuple = (0, 2, 7)
23
23
 
24
- __commit_id__ = commit_id = 'g5581f8362'
24
+ __commit_id__ = commit_id = 'g6f8c52570'
@@ -40,7 +40,12 @@ def debug_stream():
40
40
  try:
41
41
  # Wait for data from broadcast thread
42
42
  data = q.get(timeout=1.0)
43
- yield f'data: {json.dumps(data)}\n\n'
43
+ # default=str: one non-JSON-serializable value (a numpy
44
+ # scalar, a live supervision object, ...) must degrade to
45
+ # its repr, not raise - a raise here closes the stream
46
+ # that carries ALL live UI updates (debug AND viewer
47
+ # frames) for this client.
48
+ yield f'data: {json.dumps(data, default=str)}\n\n'
44
49
  except queue.Empty:
45
50
  # Send keepalive
46
51
  yield 'data: {"type": "keepalive"}\n\n'
@@ -47,6 +47,7 @@ def build_node_types_cache():
47
47
  'logic',
48
48
  'network',
49
49
  'vision',
50
+ 'supervision',
50
51
  'analysis',
51
52
  'node probes',
52
53
  'opencv'
@@ -112,13 +112,24 @@ class DebugNode(BaseNode):
112
112
  return f"<numpy array, shape={val.shape}, dtype={val.dtype}>"
113
113
  elif isinstance(val, dict):
114
114
  return {k: truncate_values(v, maxlen) for k, v in val.items()}
115
- elif isinstance(val, list):
115
+ elif isinstance(val, (list, tuple)):
116
116
  return [truncate_values(v, maxlen) for v in val]
117
+ elif isinstance(val, (np.integer, np.floating, np.bool_)):
118
+ # numpy scalars are not JSON serializable
119
+ return val.item()
120
+ elif isinstance(val, str) or val is None or isinstance(val, (int, float, bool)):
121
+ if isinstance(val, str) and len(val) > maxlen:
122
+ return val[:maxlen] + f"... [truncated, {len(val)} chars]"
123
+ return val
117
124
  else:
125
+ # Any other live object (e.g. a supervision Detections at
126
+ # payload.sv) must be stringified: a raw object stored here
127
+ # poisons json.dumps for the debug API AND kills the shared
128
+ # SSE stream that also carries image-viewer frames.
118
129
  s = str(val)
119
130
  if len(s) > maxlen:
120
131
  return s[:maxlen] + f"... [truncated, {len(s)} chars]"
121
- return val
132
+ return s
122
133
 
123
134
  display_output = truncate_values(output)
124
135
 
@@ -31,6 +31,13 @@ _info.add_bullets(
31
31
  ("Repeat Rate (fps):", "How many times per second to re-send (e.g. 1 = "
32
32
  "once per second, 20 = 20 fps)."),
33
33
  )
34
+ _info.add_header("Send Now")
35
+ _info.add_bullets(
36
+ ("Send button:", "Click the button on the node to send the uploaded "
37
+ "image on demand. Set Repeat Rate to 0 (or leave Repeat "
38
+ "Send off) and trigger the image through your flow "
39
+ "whenever you want."),
40
+ )
34
41
  _info.add_header("Outputs")
35
42
  _info.add_bullets(
36
43
  ("Output 0:", "Message with the uploaded image in payload. Emitted once "
@@ -54,8 +61,13 @@ class ImageUploadNode(BaseNode):
54
61
  ui_component = 'image-drop'
55
62
  ui_component_config = {
56
63
  'tooltip': 'Drop an image here',
64
+ 'send_action': 'send_now',
65
+ 'send_tooltip': 'Send uploaded image',
57
66
  }
58
67
 
68
+ # UI-triggerable actions (see BaseNode.actions)
69
+ actions = ['send_now']
70
+
59
71
  DEFAULT_CONFIG = {
60
72
  'repeat_send': False,
61
73
  'repeat_rate': 10,
@@ -121,6 +133,15 @@ class ImageUploadNode(BaseNode):
121
133
  if msg is not None:
122
134
  self.send(msg)
123
135
 
136
+ def send_now(self):
137
+ """UI action: send the currently uploaded image on demand.
138
+
139
+ Lets a user trigger the image through their flow whenever they click
140
+ the button, independent of Repeat Send.
141
+ """
142
+ self._send_once()
143
+ return {'sent': self._image_data is not None}
144
+
124
145
  def receive_image(self, image_bytes: bytes, filename: str = ""):
125
146
  """
126
147
  Called by the server when an image is uploaded to this node.
@@ -0,0 +1,314 @@
1
+ """OpenVINO device detection and device-string translation helpers.
2
+
3
+ This module must stay importable when openvino (and ultralytics/torch) are
4
+ NOT installed: enumeration is attempted lazily on first use and falls back to
5
+ a static device list when openvino is unavailable or enumeration fails.
6
+
7
+ Device naming background:
8
+
9
+ - ``openvino.Core().available_devices`` returns names like
10
+ ``['CPU', 'GPU', 'NPU']`` (single GPU) or ``['CPU', 'GPU.0', 'GPU.1']``
11
+ (multiple GPUs).
12
+ - PyNode/Ultralytics config values use the ``intel:<device>`` form
13
+ (``intel:gpu``, ``intel:gpu.1`` ...). Ultralytics strips the ``intel:``
14
+ prefix and passes the uppercased remainder to OpenVINO, so on a multi-GPU
15
+ system a plain ``intel:gpu`` becomes ``GPU`` which is NOT in
16
+ ``available_devices`` and Ultralytics silently falls back to ``AUTO``.
17
+ :func:`resolve_intel_device` fixes that by resolving ``intel:gpu`` to the
18
+ first detected Intel GPU (e.g. ``intel:gpu.0``).
19
+ - The GPU plugin enumerates every OpenCL-capable GPU, including non-Intel
20
+ ones it cannot compile models for (an NVIDIA card can appear as
21
+ ``GPU.1`` -> ``NVIDIA TITAN RTX (dGPU)``), so GPU entries are filtered by
22
+ their ``FULL_DEVICE_NAME`` before being offered as OpenVINO targets.
23
+
24
+ Workflows carry the device string of the machine they were built on, which
25
+ may not exist after a hardware change or an export/import.
26
+ :func:`validate_device` checks a configured device against the hardware
27
+ actually present and returns the closest usable substitute plus a warning.
28
+ """
29
+
30
+ import logging
31
+ from typing import Dict, List, Optional, Tuple
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ # Static fallback used when openvino is not importable or enumeration fails.
36
+ # It mirrors the historical hardcoded dropdown (CPU/GPU/NPU) so behavior is
37
+ # unchanged on systems without openvino.
38
+ FALLBACK_DEVICES: List[str] = ['CPU', 'GPU', 'NPU']
39
+
40
+ # Cache: (devices, detected). ``detected`` is False when the fallback list is
41
+ # in use. Enumeration can be slow, so it runs once (lazily) per process.
42
+ _device_cache: Optional[Tuple[List[str], bool]] = None
43
+
44
+ # FULL_DEVICE_NAME per enumerated device (e.g. 'GPU.0' -> 'Intel(R) UHD
45
+ # Graphics 770 (iGPU)'). Populated alongside _device_cache; empty when names
46
+ # could not be queried or the fallback list is in use.
47
+ _device_names: Dict[str, str] = {}
48
+
49
+
50
+ def _enumerate_openvino_devices() -> Optional[Tuple[List[str], Dict[str, str]]]:
51
+ """Return ``(devices, names)`` from OpenVINO, or None if unavailable."""
52
+ try:
53
+ from openvino import Core # type: ignore
54
+ except ImportError:
55
+ try:
56
+ # Older openvino releases expose Core under openvino.runtime
57
+ from openvino.runtime import Core # type: ignore
58
+ except ImportError:
59
+ return None
60
+ try:
61
+ core = Core()
62
+ devices = list(core.available_devices)
63
+ except Exception as e:
64
+ logger.warning(f"OpenVINO device enumeration failed: {e}")
65
+ return None
66
+ names: Dict[str, str] = {}
67
+ for device in devices:
68
+ try:
69
+ names[device] = str(core.get_property(device, 'FULL_DEVICE_NAME'))
70
+ except Exception:
71
+ pass
72
+ return devices, names
73
+
74
+
75
+ def get_openvino_devices(refresh: bool = False) -> Tuple[List[str], bool]:
76
+ """Return ``(devices, detected)``.
77
+
78
+ ``devices`` is e.g. ``['CPU', 'GPU.0', 'GPU.1']`` when openvino is
79
+ available, otherwise :data:`FALLBACK_DEVICES`. ``detected`` says whether
80
+ real enumeration succeeded. The result is cached after the first call.
81
+ """
82
+ global _device_cache, _device_names
83
+ if refresh or _device_cache is None:
84
+ result = _enumerate_openvino_devices()
85
+ if result and result[0]:
86
+ devices, names = result
87
+ _device_cache = (devices, True)
88
+ _device_names = names
89
+ else:
90
+ _device_cache = (list(FALLBACK_DEVICES), False)
91
+ _device_names = {}
92
+ devices, detected = _device_cache
93
+ return list(devices), detected
94
+
95
+
96
+ def get_openvino_device_names() -> Dict[str, str]:
97
+ """Return FULL_DEVICE_NAME per device (may be empty), e.g.
98
+ ``{'GPU.0': 'Intel(R) UHD Graphics 770 (iGPU)'}``."""
99
+ get_openvino_devices() # ensure enumeration has been attempted
100
+ return dict(_device_names)
101
+
102
+
103
+ def _gpu_devices(devices: List[str]) -> List[str]:
104
+ """Filter GPU entries ('GPU', 'GPU.0', ...) preserving enumeration order."""
105
+ return [d for d in devices if d == 'GPU' or d.startswith('GPU.')]
106
+
107
+
108
+ def _intel_gpu_devices(devices: List[str]) -> List[str]:
109
+ """GPU entries that are actually Intel GPUs.
110
+
111
+ OpenVINO's GPU plugin enumerates every OpenCL GPU, so on a machine with
112
+ an NVIDIA card ``available_devices`` can contain e.g. ``GPU.1`` ->
113
+ ``NVIDIA TITAN RTX (dGPU)`` even though the plugin cannot compile models
114
+ for it. Devices whose FULL_DEVICE_NAME is unknown are kept (older
115
+ openvino releases and the static fallback list carry no names).
116
+ """
117
+ names = get_openvino_device_names()
118
+ intel_gpus = []
119
+ for device in _gpu_devices(devices):
120
+ name = names.get(device)
121
+ if name is None or 'intel' in name.lower():
122
+ intel_gpus.append(device)
123
+ return intel_gpus
124
+
125
+
126
+ def get_cuda_devices() -> List[str]:
127
+ """Return ``['cuda:0', ...]`` via torch, or ``[]`` when torch is missing,
128
+ is a CPU-only build, or no NVIDIA GPU is present."""
129
+ try:
130
+ import torch # type: ignore
131
+ except ImportError:
132
+ return []
133
+ try:
134
+ if not torch.cuda.is_available():
135
+ return []
136
+ return [f'cuda:{i}' for i in range(torch.cuda.device_count())]
137
+ except Exception as e:
138
+ logger.warning(f"CUDA device enumeration failed: {e}")
139
+ return []
140
+
141
+
142
+ def get_intel_device_options() -> List[Dict[str, str]]:
143
+ """Build the Intel OpenVINO entries for the hardware dropdown.
144
+
145
+ - Intel CPU is always offered.
146
+ - One entry per detected Intel GPU (non-Intel GPUs that OpenVINO merely
147
+ enumerates via OpenCL are excluded); if there is exactly one GPU the
148
+ label has no index. Values keep the device suffix (``intel:gpu.0``
149
+ etc.) and labels carry the FULL_DEVICE_NAME when known.
150
+ - NPU is offered only when present in the device list (the static
151
+ fallback list includes it, preserving the old dropdown when openvino
152
+ is not installed).
153
+ """
154
+ devices, _detected = get_openvino_devices()
155
+ names = get_openvino_device_names()
156
+
157
+ options: List[Dict[str, str]] = [
158
+ {'value': 'intel:cpu', 'label': 'Intel CPU (OpenVINO)'},
159
+ ]
160
+
161
+ gpus = _intel_gpu_devices(devices)
162
+ for gpu in gpus:
163
+ if len(gpus) == 1:
164
+ label = 'Intel GPU (OpenVINO)'
165
+ elif '.' in gpu:
166
+ label = f'Intel GPU {gpu.split(".", 1)[1]} (OpenVINO)'
167
+ else:
168
+ label = 'Intel GPU (OpenVINO)'
169
+ if names.get(gpu):
170
+ label = f'{label} - {names[gpu]}'
171
+ options.append({
172
+ 'value': f'intel:{gpu.lower()}',
173
+ 'label': label,
174
+ })
175
+
176
+ if any(d == 'NPU' or d.startswith('NPU.') for d in devices):
177
+ options.append({'value': 'intel:npu', 'label': 'Intel NPU (OpenVINO)'})
178
+
179
+ return options
180
+
181
+
182
+ def resolve_intel_device(device: str) -> str:
183
+ """Resolve a plain ``intel:gpu`` to the first detected GPU.
184
+
185
+ ``intel:gpu`` (any case) becomes e.g. ``intel:gpu.0`` on multi-GPU
186
+ systems so the exact device - not AUTO - is targeted. Anything else
187
+ (``intel:gpu.1``, ``intel:cpu``, ``cuda:0``, ``cpu`` ...) is returned
188
+ unchanged, and when detection is unavailable the input is returned as-is
189
+ (backward compatible with saved workflows on systems without openvino).
190
+ """
191
+ if not isinstance(device, str) or device.lower() != 'intel:gpu':
192
+ return device
193
+
194
+ devices, detected = get_openvino_devices()
195
+ if not detected:
196
+ return device
197
+
198
+ gpus = _intel_gpu_devices(devices)
199
+ if not gpus:
200
+ return device
201
+ return f'intel:{gpus[0].lower()}'
202
+
203
+
204
+ def validate_device(device: str) -> Tuple[str, Optional[str]]:
205
+ """Check a configured device against the hardware present on THIS machine.
206
+
207
+ Saved workflows carry device strings from whatever machine they were
208
+ built on; after a hardware change or an export/import the configured
209
+ device may simply not exist here. Returns ``(usable_device, warning)``:
210
+ ``warning`` is None when the device is usable as-is (modulo the plain
211
+ ``intel:gpu`` -> ``intel:gpu.N`` resolution). When it is not, the closest
212
+ present device is substituted - another Intel GPU if one exists, else the
213
+ first CUDA GPU, else CPU - and the warning explains the switch so the
214
+ user can fix the config. Devices that cannot be verified (openvino not
215
+ installed, unrecognized strings) are returned unchanged.
216
+ """
217
+ if not isinstance(device, str) or not device:
218
+ return device, None
219
+ lower = device.lower()
220
+
221
+ if lower in ('cpu', 'intel:cpu'):
222
+ return device, None
223
+
224
+ if lower.startswith('cuda') or lower.isdigit():
225
+ cuda_devices = get_cuda_devices()
226
+ if not cuda_devices:
227
+ try:
228
+ import torch # type: ignore # noqa: F401
229
+ reason = ('CUDA is not available - no NVIDIA GPU, or a '
230
+ 'CPU-only PyTorch build is installed')
231
+ except ImportError:
232
+ reason = 'PyTorch is not installed'
233
+ return 'cpu', (f"Configured device '{device}' is not usable "
234
+ f"({reason}). Falling back to 'cpu'.")
235
+ index = 0
236
+ if lower.isdigit():
237
+ index = int(lower)
238
+ elif ':' in lower:
239
+ try:
240
+ index = int(lower.split(':', 1)[1])
241
+ except ValueError:
242
+ index = 0
243
+ if index >= len(cuda_devices):
244
+ return cuda_devices[0], (
245
+ f"Configured device '{device}' not found (only "
246
+ f"{len(cuda_devices)} CUDA device(s) present). Using "
247
+ f"'{cuda_devices[0]}' instead.")
248
+ return device, None
249
+
250
+ if lower.startswith('intel:'):
251
+ devices, detected = get_openvino_devices()
252
+ if not detected:
253
+ # Cannot verify without openvino enumeration; keep the existing
254
+ # pass-through behavior for saved workflows.
255
+ return device, None
256
+ target = lower.split(':', 1)[1].upper()
257
+
258
+ if target == 'GPU' or target.startswith('GPU.'):
259
+ gpus = _intel_gpu_devices(devices)
260
+ if target in gpus:
261
+ return device, None
262
+ if target == 'GPU' and gpus:
263
+ # Plain 'intel:gpu' -> first Intel GPU (not a hardware change)
264
+ return f'intel:{gpus[0].lower()}', None
265
+ if gpus:
266
+ substitute = f'intel:{gpus[0].lower()}'
267
+ names = get_openvino_device_names()
268
+ extra = ''
269
+ if target in _gpu_devices(devices) and names.get(target):
270
+ # The index exists but points at a non-Intel OpenCL GPU
271
+ extra = (f" ('{target}' is {names[target]}, which "
272
+ "OpenVINO cannot target)")
273
+ return substitute, (
274
+ f"Configured device '{device}' is not available on this "
275
+ f"machine{extra}. Using '{substitute}' instead.")
276
+ fallback = _fallback_device()
277
+ return fallback, (
278
+ f"Configured device '{device}' is not available on this "
279
+ f"machine (no Intel GPU detected by OpenVINO). Falling back "
280
+ f"to '{fallback}'.")
281
+
282
+ if target == 'NPU' or target.startswith('NPU.'):
283
+ if any(d == 'NPU' or d.startswith('NPU.') for d in devices):
284
+ return device, None
285
+ fallback = _fallback_device()
286
+ return fallback, (
287
+ f"Configured device '{device}' is not available on this "
288
+ f"machine (no Intel NPU detected). Falling back to "
289
+ f"'{fallback}'.")
290
+
291
+ return device, None
292
+
293
+ return device, None
294
+
295
+
296
+ def _fallback_device() -> str:
297
+ """Best present device to substitute for an absent one: the first CUDA
298
+ GPU when torch can use one, otherwise CPU."""
299
+ cuda_devices = get_cuda_devices()
300
+ return cuda_devices[0] if cuda_devices else 'cpu'
301
+
302
+
303
+ def to_openvino_device_name(device: str) -> str:
304
+ """Translate a config value to the OpenVINO device name Ultralytics uses.
305
+
306
+ ``intel:gpu.1`` -> ``GPU.1``; ``intel:gpu`` -> first detected GPU (e.g.
307
+ ``GPU.0``) or ``GPU`` when detection is unavailable; non-``intel:``
308
+ values are just uppercased (matching Ultralytics' behavior of
309
+ ``device.split(':')[1].upper()``).
310
+ """
311
+ resolved = resolve_intel_device(device)
312
+ if isinstance(resolved, str) and resolved.lower().startswith('intel:'):
313
+ return resolved.split(':', 1)[1].upper()
314
+ return str(resolved).upper()
@@ -31,13 +31,16 @@ except ImportError:
31
31
  from base_engine import BaseInferenceEngine
32
32
 
33
33
  try:
34
- from ..device_detection import resolve_intel_device, to_openvino_device_name
34
+ from ..device_detection import (
35
+ resolve_intel_device, to_openvino_device_name, validate_device)
35
36
  except ImportError:
36
37
  try:
37
- from InferenceEngine.device_detection import resolve_intel_device, to_openvino_device_name
38
+ from InferenceEngine.device_detection import (
39
+ resolve_intel_device, to_openvino_device_name, validate_device)
38
40
  except ImportError:
39
41
  # Standalone: inference_engine_dir was inserted into sys.path above
40
- from device_detection import resolve_intel_device, to_openvino_device_name
42
+ from device_detection import (
43
+ resolve_intel_device, to_openvino_device_name, validate_device)
41
44
 
42
45
 
43
46
  from ultralytics import YOLO
@@ -133,10 +136,13 @@ class UltralyticsEngine(BaseInferenceEngine):
133
136
  if device is None:
134
137
  device = self.device
135
138
 
136
- # Resolve a plain 'intel:gpu' (e.g. from saved workflow configs) to the
137
- # first detected GPU ('intel:gpu.0') so multi-GPU systems target a real
138
- # OpenVINO device instead of the nonexistent 'GPU' -> AUTO fallback.
139
- device = resolve_intel_device(device)
139
+ # Resolve a plain 'intel:gpu' to the first detected Intel GPU and swap
140
+ # devices that are not present on this machine (hardware change, or a
141
+ # workflow imported from another machine) for the closest present one.
142
+ device, device_warning = validate_device(device)
143
+ if device_warning:
144
+ logger.warning(device_warning)
145
+ self.use_openvino = device.lower().startswith('intel:')
140
146
 
141
147
  if model_file is None:
142
148
  model_file = self.model_path
@@ -226,6 +226,9 @@ class InferenceNode(BaseNode):
226
226
  self._current_engine_type = None
227
227
  self._current_model_path = None
228
228
  self._current_device = None
229
+ # Device actually in use after hardware validation (may differ from
230
+ # the configured device when that hardware is absent on this machine)
231
+ self._active_device = None
229
232
 
230
233
  def _get_engine_factory(self):
231
234
  """Get the inference engine factory (lazy import)."""
@@ -267,30 +270,46 @@ class InferenceNode(BaseNode):
267
270
  factory = self._get_engine_factory()
268
271
  if factory is None:
269
272
  return False
270
-
273
+
274
+ # Validate the configured device against hardware actually present
275
+ # (saved workflows can reference devices from another machine)
276
+ active_device = device
277
+ try:
278
+ from pynode.nodes.InferenceNode.InferenceEngine.device_detection import (
279
+ validate_device,
280
+ )
281
+ active_device, device_warning = validate_device(device)
282
+ if device_warning:
283
+ logger.warning(device_warning)
284
+ self.report_error(device_warning)
285
+ except Exception as e:
286
+ logger.warning(f"Device validation failed: {e}")
287
+
271
288
  try:
272
289
  # Create the engine instance
273
- logger.info(f"Creating {engine_type} engine with model: {model_path}, device: {device}")
290
+ logger.info(f"Creating {engine_type} engine with model: {model_path}, device: {active_device}")
274
291
  self.engine = factory.create(
275
292
  engine_type=engine_type,
276
293
  model_path=model_path,
277
- device=device
294
+ device=active_device
278
295
  )
279
-
296
+
280
297
  # Load the model
281
- if not self.engine.load(model_path, device):
298
+ if not self.engine.load(model_path, active_device):
282
299
  self.report_error(f"Failed to load model: {model_path}")
283
300
  self.engine = None
284
301
  self._engine_loaded = False
285
302
  return False
286
303
 
287
- # Update state
304
+ # Update state (_current_device stays the CONFIGURED value so the
305
+ # needs_reload comparison against config doesn't churn)
288
306
  self._engine_loaded = True
289
307
  self._current_engine_type = engine_type
290
308
  self._current_model_path = model_path
291
309
  self._current_device = device
292
-
293
- logger.info(f"Successfully loaded {engine_type} engine on {device}")
310
+ self._active_device = active_device
311
+
312
+ logger.info(f"Successfully loaded {engine_type} engine on {active_device}")
294
313
  return True
295
314
 
296
315
  except Exception as e:
@@ -422,7 +441,7 @@ class InferenceNode(BaseNode):
422
441
 
423
442
  # Add engine info to payload
424
443
  payload_out['engine_type'] = self._current_engine_type
425
- payload_out['device'] = self._current_device
444
+ payload_out['device'] = self._active_device or self._current_device
426
445
 
427
446
  # Preserve original message properties and update payload
428
447
  msg[MessageKeys.PAYLOAD] = payload_out
@@ -222,7 +222,20 @@ class Qwen3VLMNode(BaseNode):
222
222
  return True
223
223
 
224
224
  model_name = self.config.get("model_name", "Qwen/Qwen3-VL-2B-Instruct")
225
+ # Validate against hardware actually present: a workflow saved on a
226
+ # machine with more (or different) GPUs would otherwise hand an
227
+ # absent device straight to device_map and fail on load.
225
228
  device = self.config.get("device", "cpu")
229
+ try:
230
+ from pynode.nodes.InferenceNode.InferenceEngine.device_detection import (
231
+ validate_device,
232
+ )
233
+ device, device_warning = validate_device(device)
234
+ if device_warning:
235
+ logger.warning(device_warning)
236
+ self.report_error(device_warning)
237
+ except Exception as exc: # pragma: no cover - defensive
238
+ logger.warning("Device validation failed: %s", exc)
226
239
 
227
240
  token = self.config.get("hf_token", "").strip() or None
228
241