pynode-flow 0.2.6__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.
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/.claude/skills/pynode-workflows/SKILL.md +26 -4
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/INSTALL.md +1 -1
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/PKG-INFO +3 -2
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/README.md +2 -1
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/_version.py +3 -3
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/api/sse.py +6 -1
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/node_registry.py +1 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/DebugNode/debug_node.py +13 -2
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/ImageUploadNode/image_upload_node.py +21 -0
- pynode_flow-0.2.7/pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +314 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +13 -7
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/InferenceNode/inference_node.py +28 -9
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +13 -0
- pynode_flow-0.2.7/pynode/nodes/Supervision/__init__.py +25 -0
- pynode_flow-0.2.7/pynode/nodes/Supervision/requirements.txt +3 -0
- pynode_flow-0.2.7/pynode/nodes/Supervision/supervision_annotate_node.py +393 -0
- pynode_flow-0.2.7/pynode/nodes/Supervision/supervision_filter_node.py +278 -0
- pynode_flow-0.2.7/pynode/nodes/Supervision/supervision_line_counter_node.py +313 -0
- pynode_flow-0.2.7/pynode/nodes/Supervision/supervision_sink_node.py +223 -0
- pynode_flow-0.2.7/pynode/nodes/Supervision/supervision_smoother_node.py +146 -0
- pynode_flow-0.2.7/pynode/nodes/Supervision/supervision_tracker_node.py +229 -0
- pynode_flow-0.2.7/pynode/nodes/Supervision/supervision_zone_node.py +316 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/UltralyticsNode/ultralytics_node.py +18 -7
- pynode_flow-0.2.7/pynode/nodes/supervision_utils.py +375 -0
- pynode_flow-0.2.7/pynode/static/examples/18-track-trace.json +87 -0
- pynode_flow-0.2.7/pynode/static/examples/19-line-counter.json +110 -0
- pynode_flow-0.2.7/pynode/static/examples/20-zone-occupancy.json +112 -0
- pynode_flow-0.2.7/pynode/static/examples/21-annotator-gallery.json +170 -0
- pynode_flow-0.2.7/pynode/static/examples/22-heatmap.json +113 -0
- pynode_flow-0.2.7/pynode/static/examples/23-privacy-blur.json +95 -0
- pynode_flow-0.2.7/pynode/static/examples/24-detection-log.json +108 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/examples/README.md +29 -1
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/examples/manifest.json +49 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/js/config.js +1 -0
- pynode_flow-0.2.7/pynode/static/js/geometry-editor.js +236 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/js/main.js +2 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/js/nodes.js +7 -2
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/js/properties.js +30 -1
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/style.css +103 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode_flow.egg-info/PKG-INFO +3 -2
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode_flow.egg-info/SOURCES.txt +24 -3
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode_flow.egg-info/scm_file_list.json +296 -275
- pynode_flow-0.2.7/pynode_flow.egg-info/scm_version.json +8 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/setup.bat +55 -27
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/setup.sh +38 -15
- pynode_flow-0.2.7/tests/test_debug_json_safety.py +89 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_device_detection.py +146 -1
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_image_upload_node.py +28 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_node_types_api.py +1 -0
- pynode_flow-0.2.7/tests/test_supervision_annotate_node.py +259 -0
- pynode_flow-0.2.7/tests/test_supervision_phase3_nodes.py +293 -0
- pynode_flow-0.2.7/tests/test_supervision_tracker_node.py +226 -0
- pynode_flow-0.2.7/tests/test_supervision_utils.py +180 -0
- pynode_flow-0.2.7/tests/test_supervision_zone_nodes.py +323 -0
- pynode_flow-0.2.6/pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +0 -149
- pynode_flow-0.2.6/pynode/nodes/TrackerNode/__init__.py +0 -2
- pynode_flow-0.2.6/pynode/nodes/TrackerNode/supervision_tracker_node.py +0 -251
- pynode_flow-0.2.6/pynode/nodes/TrackerNode/tracker_node.py +0 -745
- pynode_flow-0.2.6/pynode_flow.egg-info/scm_version.json +0 -8
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/.dockerignore +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/.github/workflows/ci.yml +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/.github/workflows/publish.yaml +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/.gitignore +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/DOCKER.md +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/Dockerfile +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/Dockerfile.cpu +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/LICENSE +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/MANIFEST.in +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/docker-compose.yml +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/docker-entrypoint.sh +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/docs/CREATING_NODES.md +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/docs/CUSTOM_NODES.md +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/docs/DOCKER.md +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/docs/EXTENSIBILITY.md +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/docs/UI_COMPONENTS.md +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/examples/README.md +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/examples/camera_workflow.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/examples/camera_yolo_workflow.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/install_nodes.bat +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/install_nodes.sh +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/__main__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/_perf.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/api/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/api/helpers.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/api/nodes.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/api/services.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/api/uploads.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/api/workflows.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/config.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/install_nodes.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/main.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/AutoSyncNode/auto_sync_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/BBoxMetricsNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/BBoxMetricsNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/BatchNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/BatchNode/batch_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/BatchNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/CameraNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/CameraNode/camera_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/CameraNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/ChangeNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/ChangeNode/change_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/ChangeNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/ConfidenceFilterNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/ConfidenceFilterNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/ControlSliderNode/control_slider_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/CounterNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/CounterNode/counter_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/CropNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/CropNode/crop_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/CropNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/DebugNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/DebugNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/DelayNode/delay_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/DelayNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/DrawPredictionsNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/DrawPredictionsNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/ErrorNode/error_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/ErrorNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/FilterNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/FilterNode/filter_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/FilterNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/FrameSourceNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/FrameSourceNode/frame_source_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/FrameSourceNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/FunctionNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/FunctionNode/function_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/FunctionNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/GateNode/gate_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/GateNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/ImageFormatNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/ImageFormatNode/image_format_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/ImageFormatNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/ImageViewerNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/ImageViewerNode/image_viewer_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/ImageViewerNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/ImageWriterNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/ImageWriterNode/imagewriter_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/ImageWriterNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/InferenceNode/InferenceEngine/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/InferenceNode/InferenceEngine/readme.md +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/InferenceNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/InjectNode/inject_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/InjectNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/JoinNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/JoinNode/join_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/JoinNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/LabelFilterNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/LabelFilterNode/label_filter_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/LabelFilterNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/LinkNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/LinkNode/link_bus.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/LinkNode/link_in_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/LinkNode/link_out_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/LinkNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/MQTTNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/MQTTNode/mqtt_in_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/MQTTNode/mqtt_out_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/MQTTNode/mqtt_service.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/MQTTNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/MessageWriterNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/MessageWriterNode/messagereader_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/MessageWriterNode/messagewriter_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OmronCameraNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OmronCameraNode/omron_camera_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OmronCameraNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/arithmetic_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/bitwise_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/blend_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/blob_detector_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/blur_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/color_space_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/colormap_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/contours_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/denormalize_coords_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/draw_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/edge_detector_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/equirectangular_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/fft_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/focus_estimation_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/frequency_filter_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/histogram_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/hough_circles_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/hough_lines_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/in_range_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/morphology_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/normalize_coords_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/paste_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/perspective_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/realsense_depth_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/resize_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/rotate_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/template_match_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/OpenCV/threshold_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/PointInShapeNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/PointInShapeNode/point_in_shape_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/PointInShapeNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/PolygonMetricsNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/PolygonMetricsNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/QueueLengthProbeNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/Qwen3VLMNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/Qwen3VLMNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/RESTEndpointNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/RangeNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/RangeNode/range_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/RangeNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/RateProbeNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/RateProbeNode/rate_probe_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/RateProbeNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/RoboflowUploadNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/RoboflowUploadNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SliceNode/README.md +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SliceNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SliceNode/merge_predictions_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SliceNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SliceNode/slice_collector_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SliceNode/slice_image_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/interop/README.md +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/interop/nodered-example-flow.json +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/ndjson_protocol.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/nodered_snippets.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/tcp_in_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/tcp_out_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/udp_in_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/udp_out_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/udp_probe.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SocketNode/udp_protocol.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SplitNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SplitNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SplitNode/split_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SwitchNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SwitchNode/switch_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SyncNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/SyncNode/sync_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/TemplateNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/TemplateNode/template_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/UltralyticsNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/UltralyticsNode/model_paths.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/UltralyticsNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/UnknownNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/VLLMNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/VLLMNode/vllm_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/VideoReaderNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/VideoReaderNode/video_reader_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/VideoWriterNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/VideoWriterNode/video_writer_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/WebhookNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/WebhookNode/webhook_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/base_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/image_utils.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/info.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/mDNSNode/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/mDNSNode/mdns_broadcast_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/mDNSNode/mdns_discovery_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/mDNSNode/mdns_manager.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/mDNSNode/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/messages.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/nodes/pacing.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/server.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/examples/01-hello-world.json +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/examples/02-function-transform.json +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/examples/03-switch-routing.json +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/examples/04-flow-control.json +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/examples/05-webcam-viewer.json +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/examples/06-webcam-yolo.json +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/examples/07-blur-people.json +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/examples/08-video-yolo-recorder.json +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/examples/09-mqtt-pub-sub.json +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/examples/10-person-alert-mqtt.json +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/examples/16-socket-bridge.json +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/examples/17-slider-crop.json +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/images/favicon4.png +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/index.html +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/js/README.md +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/js/auth.js +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/js/clipboard.js +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/js/connections.js +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/js/debug.js +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/js/events.js +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/js/history.js +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/js/minimap.js +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/js/mqtt-services.js +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/js/palette.js +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/js/selection.js +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/js/state.js +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/js/ui-utils.js +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/js/viewport.js +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/js/workflow.js +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/static/js/workflows.js +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/workflow_engine.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode/workflow_manager.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode_flow.egg-info/dependency_links.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode_flow.egg-info/entry_points.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode_flow.egg-info/requires.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pynode_flow.egg-info/top_level.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/pyproject.toml +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/requirements.txt +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/setup.cfg +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/setup.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/__init__.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/conftest.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_api_auth.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_api_errors.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_api_nodes.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_api_security.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_api_workflows.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_base_node_messaging.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_camera_node_backend.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_concurrency.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_config.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_control_slider_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_counter_sse.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_crop_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_debug_workflow_scope.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_delay_rate_limit.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_frame_pacing.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_function_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_link_nodes.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_message_reader_pickle.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_models_dir.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_mqtt_services.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_node_registration.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_persistence.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_range_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_send_copy_semantics.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_serialization.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_socket_nodes.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_video_reader_node.py +0 -0
- {pynode_flow-0.2.6 → pynode_flow-0.2.7}/tests/test_workflow_engine.py +0 -0
- {pynode_flow-0.2.6 → 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`,
|
|
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
|
-
|
|
168
|
-
r = client.post('/api/
|
|
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 =
|
|
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,
|
|
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.
|
|
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,
|
|
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,
|
|
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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
21
|
+
__version__ = version = '0.2.7'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 7)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
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
|
-
|
|
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'
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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'
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
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: {
|
|
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=
|
|
294
|
+
device=active_device
|
|
278
295
|
)
|
|
279
|
-
|
|
296
|
+
|
|
280
297
|
# Load the model
|
|
281
|
-
if not self.engine.load(model_path,
|
|
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
|
-
|
|
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
|
|