pynode-flow 0.2.2__tar.gz → 0.2.3__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.3/DOCKER.md +189 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/PKG-INFO +20 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/README.md +19 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/docker-compose.yml +6 -1
- pynode_flow-0.2.3/plan-pynodeReview-2026-07-18.prompt.md +135 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/_version.py +3 -3
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/config.py +42 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/main.py +9 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ImageViewerNode/image_viewer_node.py +16 -9
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/LinkNode/link_bus.py +7 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OmronCameraNode/omron_camera_node.py +13 -3
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SwitchNode/switch_node.py +8 -2
- pynode_flow-0.2.3/pynode/nodes/UltralyticsNode/__init__.py +11 -0
- pynode_flow-0.2.3/pynode/nodes/UltralyticsNode/model_paths.py +115 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/UltralyticsNode/ultralytics_node.py +15 -9
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/__init__.py +6 -5
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/base_node.py +160 -52
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/mDNSNode/mdns_manager.py +28 -3
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/workflow_engine.py +0 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode_flow.egg-info/PKG-INFO +20 -1
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode_flow.egg-info/SOURCES.txt +4 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode_flow.egg-info/scm_file_list.json +4 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode_flow.egg-info/scm_version.json +2 -2
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pyproject.toml +7 -0
- pynode_flow-0.2.3/tests/test_models_dir.py +230 -0
- pynode_flow-0.2.3/tests/test_send_copy_semantics.py +226 -0
- pynode_flow-0.2.2/DOCKER.md +0 -113
- pynode_flow-0.2.2/pynode/nodes/UltralyticsNode/__init__.py +0 -4
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/.claude/skills/pynode-workflows/SKILL.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/.dockerignore +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/.github/workflows/ci.yml +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/.github/workflows/publish.yaml +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/.gitignore +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/Dockerfile +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/Dockerfile.cpu +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/INSTALL.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/LICENSE +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/MANIFEST.in +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/docs/CREATING_NODES.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/docs/CUSTOM_NODES.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/docs/DOCKER.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/docs/EXTENSIBILITY.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/docs/UI_COMPONENTS.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/examples/README.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/examples/camera_workflow.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/examples/camera_yolo_workflow.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/install_nodes.bat +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/install_nodes.sh +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/plan-pynodeRepoReview.prompt.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/__main__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/_perf.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/api/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/api/helpers.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/api/nodes.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/api/services.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/api/sse.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/api/uploads.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/api/workflows.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/install_nodes.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/node_registry.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/AutoSyncNode/auto_sync_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/BBoxMetricsNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/BBoxMetricsNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/BatchNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/BatchNode/batch_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/BatchNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/CameraNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/CameraNode/camera_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/CameraNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ChangeNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ChangeNode/change_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ChangeNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ConfidenceFilterNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ConfidenceFilterNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/CounterNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/CounterNode/counter_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/CropNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/CropNode/crop_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/CropNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/DebugNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/DebugNode/debug_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/DebugNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/DelayNode/delay_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/DelayNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/DrawPredictionsNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/DrawPredictionsNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ErrorNode/error_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ErrorNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/FilterNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/FilterNode/filter_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/FilterNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/FrameSourceNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/FrameSourceNode/frame_source_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/FrameSourceNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/FunctionNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/FunctionNode/function_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/FunctionNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/GateNode/gate_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/GateNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ImageFormatNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ImageFormatNode/image_format_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ImageFormatNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ImageUploadNode/image_upload_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ImageViewerNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ImageViewerNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ImageWriterNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ImageWriterNode/imagewriter_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/ImageWriterNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/readme.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/inference_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InferenceNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InjectNode/inject_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/InjectNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/JoinNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/JoinNode/join_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/JoinNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/LabelFilterNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/LabelFilterNode/label_filter_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/LabelFilterNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/LinkNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/LinkNode/link_in_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/LinkNode/link_out_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/LinkNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/MQTTNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/MQTTNode/mqtt_in_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/MQTTNode/mqtt_out_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/MQTTNode/mqtt_service.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/MQTTNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/MessageWriterNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/MessageWriterNode/messagereader_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/MessageWriterNode/messagewriter_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OmronCameraNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OmronCameraNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/arithmetic_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/bitwise_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/blend_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/blob_detector_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/blur_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/color_space_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/colormap_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/contours_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/denormalize_coords_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/draw_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/edge_detector_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/equirectangular_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/fft_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/focus_estimation_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/frequency_filter_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/histogram_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/hough_circles_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/hough_lines_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/in_range_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/morphology_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/normalize_coords_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/paste_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/perspective_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/realsense_depth_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/resize_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/rotate_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/template_match_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/OpenCV/threshold_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/PointInShapeNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/PointInShapeNode/point_in_shape_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/PointInShapeNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/PolygonMetricsNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/PolygonMetricsNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/QueueLengthProbeNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/Qwen3VLMNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/Qwen3VLMNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/RESTEndpointNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/RangeNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/RangeNode/range_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/RangeNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/RateProbeNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/RateProbeNode/rate_probe_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/RateProbeNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/RoboflowUploadNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/RoboflowUploadNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SliceNode/README.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SliceNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SliceNode/merge_predictions_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SliceNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SliceNode/slice_collector_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SliceNode/slice_image_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SplitNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SplitNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SplitNode/split_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SwitchNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SyncNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/SyncNode/sync_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/TemplateNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/TemplateNode/template_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/TrackerNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/TrackerNode/supervision_tracker_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/TrackerNode/tracker_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/UltralyticsNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/UnknownNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/VLLMNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/VLLMNode/vllm_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/VideoReaderNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/VideoReaderNode/video_reader_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/VideoWriterNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/VideoWriterNode/video_writer_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/WebhookNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/WebhookNode/webhook_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/image_utils.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/info.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/mDNSNode/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/mDNSNode/mdns_broadcast_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/mDNSNode/mdns_discovery_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/mDNSNode/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/nodes/messages.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/server.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/01-hello-world.json +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/02-function-transform.json +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/03-switch-routing.json +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/04-flow-control.json +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/05-webcam-viewer.json +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/06-webcam-yolo.json +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/07-blur-people.json +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/08-video-yolo-recorder.json +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/09-mqtt-pub-sub.json +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/10-person-alert-mqtt.json +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/README.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/examples/manifest.json +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/images/favicon4.png +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/index.html +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/README.md +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/auth.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/clipboard.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/config.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/connections.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/debug.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/events.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/history.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/main.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/minimap.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/mqtt-services.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/nodes.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/palette.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/properties.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/selection.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/state.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/ui-utils.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/viewport.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/workflow.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/js/workflows.js +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/static/style.css +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode/workflow_manager.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode_flow.egg-info/dependency_links.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode_flow.egg-info/entry_points.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode_flow.egg-info/requires.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/pynode_flow.egg-info/top_level.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/requirements.txt +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/setup.bat +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/setup.cfg +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/setup.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/setup.sh +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/__init__.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/conftest.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_api_auth.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_api_errors.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_api_nodes.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_api_security.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_api_workflows.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_base_node_messaging.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_camera_node_backend.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_concurrency.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_config.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_counter_sse.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_debug_workflow_scope.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_delay_rate_limit.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_device_detection.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_link_nodes.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_message_reader_pickle.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_mqtt_services.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_node_registration.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_node_types_api.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_persistence.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_serialization.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_video_reader_node.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_workflow_engine.py +0 -0
- {pynode_flow-0.2.2 → pynode_flow-0.2.3}/tests/test_workflow_roundtrip.py +0 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Docker Setup for PyNode
|
|
2
|
+
|
|
3
|
+
## Prerequisites
|
|
4
|
+
|
|
5
|
+
- Docker Engine with GPU support (NVIDIA Container Toolkit)
|
|
6
|
+
- Docker Compose v1.28+ (for GPU support)
|
|
7
|
+
- NVIDIA GPU with CUDA support
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
### Build and Run
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
docker-compose up --build
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Run in Background
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
docker-compose up -d
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### View Logs
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
docker-compose logs -f
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Stop the Application
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
docker-compose down
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
### Ports
|
|
38
|
+
|
|
39
|
+
The application runs on port `5000` by default. To change the port, edit `docker-compose.yml`:
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
ports:
|
|
43
|
+
- "8080:5000" # Change 8080 to your desired port
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### GPU Support
|
|
47
|
+
|
|
48
|
+
The Docker Compose file is configured to use all available NVIDIA GPUs. To limit GPU access:
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
deploy:
|
|
52
|
+
resources:
|
|
53
|
+
reservations:
|
|
54
|
+
devices:
|
|
55
|
+
- driver: nvidia
|
|
56
|
+
device_ids: ['0'] # Use only GPU 0
|
|
57
|
+
capabilities: [gpu]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Persistent Storage
|
|
61
|
+
|
|
62
|
+
The following directories are mounted for persistence:
|
|
63
|
+
- `./workflows` - Saved workflows
|
|
64
|
+
- `./logs` - Application logs
|
|
65
|
+
|
|
66
|
+
## Building
|
|
67
|
+
|
|
68
|
+
### Custom CUDA Version
|
|
69
|
+
|
|
70
|
+
To use a different CUDA version, edit the `Dockerfile`:
|
|
71
|
+
|
|
72
|
+
```dockerfile
|
|
73
|
+
FROM nvidia/cuda:12.8.0-runtime-ubuntu22.04 # Change CUDA version
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Without GPU Support
|
|
77
|
+
|
|
78
|
+
For CPU-only deployment, use the CPU Dockerfile:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
docker build -f Dockerfile.cpu -t pynode-cpu .
|
|
82
|
+
docker run -p 5000:5000 pynode-cpu
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Accessing the Application
|
|
86
|
+
|
|
87
|
+
Once running, access the web interface at:
|
|
88
|
+
- http://localhost:5000
|
|
89
|
+
|
|
90
|
+
## mDNS / Service Discovery
|
|
91
|
+
|
|
92
|
+
### The Limitation
|
|
93
|
+
|
|
94
|
+
The default `docker-compose.yml` uses Docker's standard bridge network. mDNS
|
|
95
|
+
(multicast DNS) relies on link-local multicast traffic (UDP `224.0.0.251:5353`),
|
|
96
|
+
which the bridge network's NAT does not forward in either direction. With the
|
|
97
|
+
default compose file, PyNode's mDNS nodes can broadcast and listen *inside*
|
|
98
|
+
the container, but that traffic never reaches the LAN — so other machines on
|
|
99
|
+
your network cannot discover the container, and the container cannot discover
|
|
100
|
+
services running elsewhere.
|
|
101
|
+
|
|
102
|
+
### `HOST_IP`
|
|
103
|
+
|
|
104
|
+
The `HOST_IP` environment variable (see `docker-compose.yml`) only controls
|
|
105
|
+
the IP address **advertised** in mDNS service records - it does *not* make
|
|
106
|
+
multicast traffic reach the LAN. Setting `HOST_IP` without one of the fixes
|
|
107
|
+
below just means the (unreachable) mDNS records will advertise the right
|
|
108
|
+
address instead of the container's internal one; it does not fix discovery.
|
|
109
|
+
|
|
110
|
+
### Fixing It
|
|
111
|
+
|
|
112
|
+
**Option 1: `network_mode: host` (Linux hosts only, full fix)**
|
|
113
|
+
|
|
114
|
+
On Linux, host networking removes the network isolation between the
|
|
115
|
+
container and the host, so multicast works exactly as it would for a
|
|
116
|
+
process running directly on the host:
|
|
117
|
+
|
|
118
|
+
```yaml
|
|
119
|
+
services:
|
|
120
|
+
pynode:
|
|
121
|
+
network_mode: host
|
|
122
|
+
# ports: mapping is ignored in host mode - the container shares the
|
|
123
|
+
# host's network stack directly, so remove/comment out `ports:` above.
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
This does **not** work on Docker Desktop for Windows/macOS - Docker Desktop
|
|
127
|
+
runs containers inside a lightweight VM, and `network_mode: host` there still
|
|
128
|
+
does not bridge multicast across the VM boundary.
|
|
129
|
+
|
|
130
|
+
**Option 2: macvlan network**
|
|
131
|
+
|
|
132
|
+
A macvlan network gives the container its own IP address directly on the LAN
|
|
133
|
+
(as if it were a separate physical device), which also allows multicast to
|
|
134
|
+
work normally. This works on Docker Desktop as well as Linux, but requires
|
|
135
|
+
more setup (a dedicated IP, and typically a way for the host itself to reach
|
|
136
|
+
the container since macvlan interfaces are not reachable from the host by
|
|
137
|
+
default). See the
|
|
138
|
+
[Docker macvlan network documentation](https://docs.docker.com/network/drivers/macvlan/)
|
|
139
|
+
for setup details.
|
|
140
|
+
|
|
141
|
+
**Option 3: mDNS reflector on the host**
|
|
142
|
+
|
|
143
|
+
When host networking isn't available (e.g. Docker Desktop), run an mDNS
|
|
144
|
+
reflector on the host to bridge multicast between the host's network and the
|
|
145
|
+
container's bridge network:
|
|
146
|
+
|
|
147
|
+
- **avahi-daemon** (Linux) - enable reflection in `/etc/avahi/avahi-daemon.conf`:
|
|
148
|
+
```ini
|
|
149
|
+
[reflector]
|
|
150
|
+
enable-reflector=yes
|
|
151
|
+
```
|
|
152
|
+
- **mdns-repeater** - run as a sidecar/host process that repeats mDNS packets
|
|
153
|
+
between interfaces.
|
|
154
|
+
|
|
155
|
+
This is the option that works when host networking isn't an option, at the
|
|
156
|
+
cost of an extra moving part to run and maintain.
|
|
157
|
+
|
|
158
|
+
### Port Matching
|
|
159
|
+
|
|
160
|
+
Whichever option you use, the port PyNode advertises in its mDNS records
|
|
161
|
+
must match the port other machines can actually reach it on (the *published*
|
|
162
|
+
port), not just the container's internal port. If you remap ports (e.g.
|
|
163
|
+
`"8080:5000"`), make sure discovery consumers connect using the published
|
|
164
|
+
port (`8080`), not the internal one.
|
|
165
|
+
|
|
166
|
+
## Troubleshooting
|
|
167
|
+
|
|
168
|
+
### GPU Not Detected
|
|
169
|
+
|
|
170
|
+
Verify NVIDIA Container Toolkit is installed:
|
|
171
|
+
```bash
|
|
172
|
+
docker run --rm --gpus all nvidia/cuda:12.6.0-base-ubuntu22.04 nvidia-smi
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Permission Issues
|
|
176
|
+
|
|
177
|
+
Ensure the mounted directories have proper permissions:
|
|
178
|
+
```bash
|
|
179
|
+
mkdir -p workflows logs
|
|
180
|
+
chmod 777 workflows logs
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Rebuild from Scratch
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
docker-compose down
|
|
187
|
+
docker-compose build --no-cache
|
|
188
|
+
docker-compose up
|
|
189
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pynode-flow
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Python based Node-RED-like visual workflow system
|
|
5
5
|
Author: olkham
|
|
6
6
|
License-Expression: MIT
|
|
@@ -214,6 +214,25 @@ PyNode persists workflows under `<data dir>/workflows/` (`workflow.json` plus ti
|
|
|
214
214
|
|
|
215
215
|
The resolved location is logged at startup (`Workflow data directory: ...`).
|
|
216
216
|
|
|
217
|
+
### Models Directory
|
|
218
|
+
|
|
219
|
+
Nodes that download or generate model weights (e.g. the YOLO node's `.pt`
|
|
220
|
+
files and exported OpenVINO models) write them into a shared **models
|
|
221
|
+
directory** instead of the process working directory. It is resolved in this
|
|
222
|
+
order:
|
|
223
|
+
|
|
224
|
+
1. `pynode --models-dir <path>` CLI flag,
|
|
225
|
+
2. `PYNODE_MODELS_DIR` environment variable,
|
|
226
|
+
3. `<data dir>/models` otherwise — so `<repo>/models` for a source checkout and
|
|
227
|
+
`~/.pynode/models` for a regular `pip install`.
|
|
228
|
+
|
|
229
|
+
> **Upgrading:** older PyNode versions downloaded model files into whatever the
|
|
230
|
+
> working directory happened to be, so stray `.pt` files and
|
|
231
|
+
> `*_openvino_model/` folders may exist in the repo root, `pynode/models/` or
|
|
232
|
+
> `pynode/nodes/`. PyNode still reads models from those legacy locations, but
|
|
233
|
+
> nothing is migrated automatically — you can move them into the models
|
|
234
|
+
> directory manually when convenient.
|
|
235
|
+
|
|
217
236
|
## Securing PyNode
|
|
218
237
|
|
|
219
238
|
**PyNode executes arbitrary Python by design** (e.g. FunctionNode runs whatever code is in the workflow), so anyone who can reach the API can run code on the host. Authentication is the trust boundary — secure the server before exposing it beyond your own machine:
|
|
@@ -149,6 +149,25 @@ PyNode persists workflows under `<data dir>/workflows/` (`workflow.json` plus ti
|
|
|
149
149
|
|
|
150
150
|
The resolved location is logged at startup (`Workflow data directory: ...`).
|
|
151
151
|
|
|
152
|
+
### Models Directory
|
|
153
|
+
|
|
154
|
+
Nodes that download or generate model weights (e.g. the YOLO node's `.pt`
|
|
155
|
+
files and exported OpenVINO models) write them into a shared **models
|
|
156
|
+
directory** instead of the process working directory. It is resolved in this
|
|
157
|
+
order:
|
|
158
|
+
|
|
159
|
+
1. `pynode --models-dir <path>` CLI flag,
|
|
160
|
+
2. `PYNODE_MODELS_DIR` environment variable,
|
|
161
|
+
3. `<data dir>/models` otherwise — so `<repo>/models` for a source checkout and
|
|
162
|
+
`~/.pynode/models` for a regular `pip install`.
|
|
163
|
+
|
|
164
|
+
> **Upgrading:** older PyNode versions downloaded model files into whatever the
|
|
165
|
+
> working directory happened to be, so stray `.pt` files and
|
|
166
|
+
> `*_openvino_model/` folders may exist in the repo root, `pynode/models/` or
|
|
167
|
+
> `pynode/nodes/`. PyNode still reads models from those legacy locations, but
|
|
168
|
+
> nothing is migrated automatically — you can move them into the models
|
|
169
|
+
> directory manually when convenient.
|
|
170
|
+
|
|
152
171
|
## Securing PyNode
|
|
153
172
|
|
|
154
173
|
**PyNode executes arbitrary Python by design** (e.g. FunctionNode runs whatever code is in the workflow), so anyone who can reach the API can run code on the host. Authentication is the trust boundary — secure the server before exposing it beyond your own machine:
|
|
@@ -5,7 +5,12 @@ services:
|
|
|
5
5
|
dockerfile: Dockerfile
|
|
6
6
|
container_name: pynode
|
|
7
7
|
ports:
|
|
8
|
-
- "
|
|
8
|
+
- "5000:5000"
|
|
9
|
+
# Uncomment for mDNS support on Linux hosts (see DOCKER.md > mDNS /
|
|
10
|
+
# Service Discovery). Not effective on Docker Desktop for Windows/macOS.
|
|
11
|
+
# Remove the `ports:` mapping above when enabling this - it is ignored
|
|
12
|
+
# in host networking mode.
|
|
13
|
+
# network_mode: host
|
|
9
14
|
volumes:
|
|
10
15
|
# Mount persistent data
|
|
11
16
|
- ./workflows:/app/workflows
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# PyNode Project Review Plan — 2026-07-18
|
|
2
|
+
|
|
3
|
+
Follow-up to the July 2026 overhaul (see `plan-pynodeRepoReview.prompt.md`). This plan captures
|
|
4
|
+
the full findings of the 2026-07-18 project review. Phases A–D are being executed now;
|
|
5
|
+
E–H are deferred and can be picked up in any later session.
|
|
6
|
+
|
|
7
|
+
Status legend: `[NOW]` in progress this session · `[LATER]` deferred · `[DECIDE]` needs a user decision.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Phase A [NOW] — Quick bug fixes + compose port
|
|
12
|
+
|
|
13
|
+
1. **Dead `include_predictions` toggle** — `pynode/nodes/UltralyticsNode/ultralytics_node.py:348`
|
|
14
|
+
reads `if include_predictions or True:` so the "Include Predictions in Output" checkbox does
|
|
15
|
+
nothing. Honor the flag (use `get_config_bool` for string-safety; same for `include_image`).
|
|
16
|
+
2. **docker-compose port typo** — `docker-compose.yml` publishes `"500:5000"`; DOCKER.md documents
|
|
17
|
+
port 5000. Change to `"5000:5000"`.
|
|
18
|
+
3. **Silent node-import failures** — `pynode/nodes/__init__.py` `_try_import_python_files` swallows
|
|
19
|
+
every exception with bare `continue`; a custom node with a syntax error vanishes from the palette
|
|
20
|
+
with no log output. Log warnings (missing optional deps may stay at debug level).
|
|
21
|
+
4. Minor cleanups: dead `system_error_node` variable in `workflow_engine.py:367`; ImageViewerNode
|
|
22
|
+
MJPEG generator loops forever after node stop (holds a waitress thread); ImageViewerNode's shared
|
|
23
|
+
`last_sent_timestamp` makes the SSE broadcaster and the `/frame` polling route steal frames from
|
|
24
|
+
each other; `socket.inet_ntoa` in `mdns_manager.py` throws on IPv6 service records.
|
|
25
|
+
|
|
26
|
+
Acceptance: full test suite passes; toggling "Include Predictions" actually changes output;
|
|
27
|
+
node import errors appear in the log.
|
|
28
|
+
|
|
29
|
+
## Phase B [NOW] — Model / node storage directories
|
|
30
|
+
|
|
31
|
+
Problem: `YOLO('yolo26n.pt')` downloads weights into the process CWD and `export(format='openvino')`
|
|
32
|
+
writes next to the source; strays currently exist in 4 places (repo root, `models/`,
|
|
33
|
+
`pynode/models/`, `pynode/nodes/`). A pip-installed PyNode launched from inside a venv would scatter
|
|
34
|
+
files there. Also `[tool.setuptools.package-data] nodes/**/*` would bake a stray `.pt` in
|
|
35
|
+
`pynode/nodes/` into a locally built wheel.
|
|
36
|
+
|
|
37
|
+
1. `pynode/config.py`: add `PYNODE_MODELS_DIR` env var + `resolve_models_dir()` → default
|
|
38
|
+
`<data_dir>/models/`; `--models-dir` CLI flag in `main.py` (mirror the `--data-dir` pattern).
|
|
39
|
+
2. `BaseNode.get_storage_dir(subdir=None)` → `<data_dir>/node_storage/<NodeType>/`, created lazily.
|
|
40
|
+
General rule going forward: nodes never write binaries outside their storage dir.
|
|
41
|
+
3. UltralyticsNode: resolve bare model names to an absolute path under the models dir before calling
|
|
42
|
+
`YOLO()` (Ultralytics downloads named assets to an absolute path; the OpenVINO export then lands
|
|
43
|
+
in the same folder). Check legacy locations first to avoid re-downloading; never move/delete
|
|
44
|
+
existing user files automatically (`pynode/models/` holds real user models).
|
|
45
|
+
4. Packaging: `[tool.setuptools.exclude-package-data]` for `*.pt` / `*_openvino_model` so strays
|
|
46
|
+
can't enter wheels.
|
|
47
|
+
5. Document (README/INSTALL): env var, CLI flag, where models live per install type.
|
|
48
|
+
|
|
49
|
+
Deferred idea: settings UI for the models folder.
|
|
50
|
+
|
|
51
|
+
## Phase C [NOW] — mDNS in Docker: fix + docs
|
|
52
|
+
|
|
53
|
+
Root cause: default bridge network; mDNS is link-local multicast (224.0.0.251:5353) which Docker's
|
|
54
|
+
bridge/NAT does not forward. `HOST_IP` only fixes the advertised address, not reachability.
|
|
55
|
+
|
|
56
|
+
1. DOCKER.md: new "mDNS / service discovery" section — explain the limitation; document `HOST_IP`
|
|
57
|
+
(currently undocumented); give options in order: `network_mode: host` (Linux; ports mapping
|
|
58
|
+
ignored; does NOT help on Docker Desktop Win/Mac), macvlan, host-side avahi reflector
|
|
59
|
+
(`enable-reflector=yes`) or mdns-repeater sidecar.
|
|
60
|
+
2. docker-compose.yml: commented-out `network_mode: host` alternative block with the caveats.
|
|
61
|
+
3. Advertised `service_port` must match the externally reachable port (relates to A2).
|
|
62
|
+
|
|
63
|
+
## Phase D [NOW] — send() copy semantics (biggest perf win)
|
|
64
|
+
|
|
65
|
+
`BaseNode.send()` deep-copies the message per recipient (`base_node.py:254`); raw numpy frames make
|
|
66
|
+
this ~6 MB memcpy per hop per recipient. Adopt Node-RED semantics: **last recipient gets the
|
|
67
|
+
original message, only additional wires get deep copies** — zero frame copies in the common
|
|
68
|
+
single-connection case. Contract change: *senders must not mutate a message after calling send().*
|
|
69
|
+
|
|
70
|
+
1. Audit every node for post-`send()` mutation and source nodes for numpy buffer reuse; fix
|
|
71
|
+
offenders to create fresh messages instead.
|
|
72
|
+
2. Implement last-recipient-no-copy in both the queued path and the sink "direct processing" path.
|
|
73
|
+
3. Update/adjust tests that assert sender-side isolation; document the contract in base_node.py.
|
|
74
|
+
|
|
75
|
+
## Phase E [LATER] — Remaining performance items
|
|
76
|
+
|
|
77
|
+
- Sink "direct processing" runs on the sender's thread (ImageViewer JPEG encode inside the camera
|
|
78
|
+
loop) and skips `drop_while_busy`; route sinks through the normal queue or apply drop checks.
|
|
79
|
+
- Idle worker threads busy-poll every 10–100 ms (`base_node.py:296`); switch to blocking
|
|
80
|
+
`queue.get()` with a `None` sentinel pushed in `on_stop()`.
|
|
81
|
+
- `sort_msg_keys` runs on every send per recipient; sort at display time in DebugNode instead.
|
|
82
|
+
- Waitress `threads=10` (`main.py:103`): each SSE client and MJPEG viewer permanently holds a
|
|
83
|
+
thread → silent API starvation. Make threads a CLI flag, default higher (~24), document.
|
|
84
|
+
- Dockerfile uses `cuda:12.6.0-devel` (~8 GB base); switch to `-runtime` or multi-stage.
|
|
85
|
+
|
|
86
|
+
## Phase F [LATER] — UI/UX
|
|
87
|
+
|
|
88
|
+
- Canvas node search (Ctrl+F: find by name/type, pan to node).
|
|
89
|
+
- Per-node runtime status text on canvas (Node-RED `status()` equivalent: "model loaded", fps,
|
|
90
|
+
drop count) — SSE plumbing (`sse_handlers`) already exists.
|
|
91
|
+
- Palette manager UI (falls out of Phase G).
|
|
92
|
+
- Light theme (style.css is dark-only; tokenize colors to CSS variables first).
|
|
93
|
+
- Groups/subflows — biggest structural gap vs Node-RED; affects workflow JSON schema, decide
|
|
94
|
+
deliberately before other schema work.
|
|
95
|
+
|
|
96
|
+
## Phase G [LATER] — Live node install from an approved registry
|
|
97
|
+
|
|
98
|
+
Constraints today: registries built once at import (`node_registry.py:154-156`); node `api_routes`
|
|
99
|
+
registered as real Flask routes at app creation (`api/nodes.py:418`, frozen after first request).
|
|
100
|
+
|
|
101
|
+
1. **Registry**: curated JSON index in a separate GitHub repo (`pynode-registry`), served raw:
|
|
102
|
+
name, description, pip requirement/git URL, version, author, category, `verified` flag.
|
|
103
|
+
Approval = PR review.
|
|
104
|
+
2. **External nodes dir**: also scan `<data_dir>/nodes/` (Node-RED `~/.node-red` pattern) —
|
|
105
|
+
cheap, enables "drop a folder in, restart". Later: `[project.entry-points."pynode.nodes"]`.
|
|
106
|
+
3. **API**: `GET /api/palette/catalog` (cached), `POST /api/palette/install` (pip via
|
|
107
|
+
`sys.executable -m pip`, reuse install_nodes.py pattern), `DELETE` for removal.
|
|
108
|
+
4. **Activation v1**: install + "restart to activate" banner.
|
|
109
|
+
**v2 (hot reload)**: make cache builders re-runnable, `palette-updated` SSE event, convert
|
|
110
|
+
per-node API routes to one catch-all `/api/nodes/<node_id>/<path:sub>` dispatching through the
|
|
111
|
+
registry dict at request time (removes the Flask route-freeze problem).
|
|
112
|
+
5. **Security**: only names from the curated index, pinned versions, behind the API key. Never
|
|
113
|
+
accept arbitrary pip specs from the UI.
|
|
114
|
+
|
|
115
|
+
## Phase H [DECIDE] — Naming
|
|
116
|
+
|
|
117
|
+
- **GitHub repo rename** `olkham/pynode` → `olkham/pynode-flow`: recommended; cheap, GitHub
|
|
118
|
+
redirects, removes PyPI/repo mismatch. Update `[project.urls]`, README.
|
|
119
|
+
- **Import package rename** (`pynode` → `pynode_flow`): argument for = the unrelated PyPI `pynode`
|
|
120
|
+
0.1.0 namespace-collision trap (see 2026-07-18 incident); argument against = breaks all imports
|
|
121
|
+
and custom nodes; mismatched dist/import names are normal (opencv-python/cv2).
|
|
122
|
+
Recommendation: keep `pynode` import; add a startup guard that detects the conflicting `pynode`
|
|
123
|
+
dist (importlib.metadata) and fails loudly with "uninstall the unrelated pynode package".
|
|
124
|
+
If ever renaming, the pre-1.0 window is the only sane time.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Execution notes (any session picking this up)
|
|
129
|
+
|
|
130
|
+
- User commits phases themselves; do not commit.
|
|
131
|
+
- Test safety (hard rules, see memory `pynode-test-safety-incident`): tests must build apps via
|
|
132
|
+
`create_app` with tmp paths (tests/conftest.py `api_client`); join every thread before fixture
|
|
133
|
+
teardown; after test work verify `git status --porcelain -- workflows/` is empty. A live PyNode
|
|
134
|
+
server may be running against this repo — its own saves to workflows/ are not test leakage.
|
|
135
|
+
- Suite size at time of writing: 306 tests.
|
|
@@ -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.3'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 3)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g6a8c7c6b1'
|
|
@@ -11,6 +11,11 @@ Environment variables
|
|
|
11
11
|
are persisted under ``<data dir>/workflows/`` (``workflow.json`` plus the
|
|
12
12
|
``_backups/`` folder). Overridden by the ``--data-dir`` CLI flag. See
|
|
13
13
|
:func:`resolve_data_dir` for the full precedence.
|
|
14
|
+
- ``PYNODE_MODELS_DIR``: directory for shared model weights (e.g. YOLO
|
|
15
|
+
``.pt`` files and exported OpenVINO models). Nodes must download/read model
|
|
16
|
+
binaries here instead of scattering them into the process CWD. Defaults to
|
|
17
|
+
``<data dir>/models``. Overridden by the ``--models-dir`` CLI flag. See
|
|
18
|
+
:func:`resolve_models_dir` for the full precedence.
|
|
14
19
|
- ``PYNODE_API_KEY``: API key required on all ``/api/`` requests
|
|
15
20
|
(``X-API-Key`` header or ``api_key`` query parameter). Empty/unset = auth
|
|
16
21
|
disabled. Overridden by the ``--api-key`` CLI flag.
|
|
@@ -23,6 +28,7 @@ import os
|
|
|
23
28
|
|
|
24
29
|
# Environment variable names (single source of truth).
|
|
25
30
|
ENV_DATA_DIR = 'PYNODE_DATA_DIR'
|
|
31
|
+
ENV_MODELS_DIR = 'PYNODE_MODELS_DIR'
|
|
26
32
|
ENV_API_KEY = 'PYNODE_API_KEY'
|
|
27
33
|
ENV_CORS_ORIGINS = 'PYNODE_CORS_ORIGINS'
|
|
28
34
|
|
|
@@ -37,6 +43,9 @@ CHECKOUT_DIR = os.path.dirname(PKG_DIR)
|
|
|
37
43
|
# Name of the workflows subdirectory inside the data dir.
|
|
38
44
|
WORKFLOWS_SUBDIR = 'workflows'
|
|
39
45
|
|
|
46
|
+
# Name of the models subdirectory inside the data dir (shared model weights).
|
|
47
|
+
MODELS_SUBDIR = 'models'
|
|
48
|
+
|
|
40
49
|
|
|
41
50
|
def _is_source_checkout(base_dir):
|
|
42
51
|
"""True if ``base_dir`` looks like a writable PyNode source checkout.
|
|
@@ -91,3 +100,36 @@ def resolve_workflows_dir(cli_data_dir=None, environ=None, checkout_dir=None):
|
|
|
91
100
|
resolve_data_dir(cli_data_dir=cli_data_dir, environ=environ,
|
|
92
101
|
checkout_dir=checkout_dir),
|
|
93
102
|
WORKFLOWS_SUBDIR)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def resolve_models_dir(cli_models_dir=None, environ=None, checkout_dir=None):
|
|
106
|
+
"""Resolve the shared models directory for downloaded model weights.
|
|
107
|
+
|
|
108
|
+
Nodes that download or generate model binaries (YOLO ``.pt`` files,
|
|
109
|
+
exported OpenVINO models, etc.) must place them here rather than in the
|
|
110
|
+
process CWD, so a pip-installed PyNode launched from a read-only or
|
|
111
|
+
arbitrary directory does not scatter files around.
|
|
112
|
+
|
|
113
|
+
Precedence:
|
|
114
|
+
|
|
115
|
+
1. ``cli_models_dir`` (the ``--models-dir`` CLI flag),
|
|
116
|
+
2. the ``PYNODE_MODELS_DIR`` environment variable,
|
|
117
|
+
3. ``<resolve_data_dir(...)>/models`` (the ``models`` subdirectory of the
|
|
118
|
+
resolved data dir — for a source checkout this is ``<repo>/models``,
|
|
119
|
+
for a pip install ``~/.pynode/models``).
|
|
120
|
+
|
|
121
|
+
Like :func:`resolve_data_dir`, the directory is only resolved here, never
|
|
122
|
+
created — creation happens lazily at the point of use.
|
|
123
|
+
"""
|
|
124
|
+
environ = os.environ if environ is None else environ
|
|
125
|
+
|
|
126
|
+
if cli_models_dir:
|
|
127
|
+
return os.path.abspath(os.path.expanduser(cli_models_dir))
|
|
128
|
+
|
|
129
|
+
env_dir = environ.get(ENV_MODELS_DIR)
|
|
130
|
+
if env_dir:
|
|
131
|
+
return os.path.abspath(os.path.expanduser(env_dir))
|
|
132
|
+
|
|
133
|
+
return os.path.join(
|
|
134
|
+
resolve_data_dir(environ=environ, checkout_dir=checkout_dir),
|
|
135
|
+
MODELS_SUBDIR)
|
|
@@ -57,6 +57,11 @@ def main():
|
|
|
57
57
|
'Also settable via the PYNODE_DATA_DIR env var. '
|
|
58
58
|
'Default: the source checkout root when running '
|
|
59
59
|
'from a checkout, otherwise ~/.pynode.')
|
|
60
|
+
parser.add_argument('--models-dir', default=None,
|
|
61
|
+
help='Directory for shared model weights (YOLO .pt '
|
|
62
|
+
'files, exported OpenVINO models, etc.). '
|
|
63
|
+
'Also settable via the PYNODE_MODELS_DIR env var. '
|
|
64
|
+
'Default: <data-dir>/models.')
|
|
60
65
|
args = parser.parse_args()
|
|
61
66
|
|
|
62
67
|
# Configure application-wide logging
|
|
@@ -74,6 +79,10 @@ def main():
|
|
|
74
79
|
os.environ['PYNODE_CORS_ORIGINS'] = args.cors_origins
|
|
75
80
|
if args.data_dir is not None:
|
|
76
81
|
os.environ['PYNODE_DATA_DIR'] = args.data_dir
|
|
82
|
+
# Node code (which has no Flask app context) resolves the models dir from
|
|
83
|
+
# the env var / defaults, so export the CLI flag before the server import.
|
|
84
|
+
if args.models_dir is not None:
|
|
85
|
+
os.environ['PYNODE_MODELS_DIR'] = args.models_dir
|
|
77
86
|
|
|
78
87
|
from pynode.server import app, load_workflow_from_disk
|
|
79
88
|
|
|
@@ -88,7 +88,8 @@ class ImageViewerNode(BaseNode):
|
|
|
88
88
|
super().__init__(node_id, name)
|
|
89
89
|
self.current_frame = None
|
|
90
90
|
self.frame_timestamp = 0
|
|
91
|
-
self.last_sent_timestamp = 0
|
|
91
|
+
self.last_sent_timestamp = 0 # dedupe state for the polled GET 'frame' route
|
|
92
|
+
self._sse_last_sent = 0 # dedupe state for the SSE 'frame' broadcaster
|
|
92
93
|
|
|
93
94
|
def on_input(self, msg: Dict[str, Any], input_index: int = 0):
|
|
94
95
|
"""
|
|
@@ -126,8 +127,10 @@ class ImageViewerNode(BaseNode):
|
|
|
126
127
|
|
|
127
128
|
def get_current_frame(self):
|
|
128
129
|
"""
|
|
129
|
-
Get the current frame for display in the UI.
|
|
130
|
-
Only returns frames that haven't been sent yet.
|
|
130
|
+
Get the current frame for display in the UI via the polled GET 'frame' route.
|
|
131
|
+
Only returns frames that haven't been sent yet via this route. Tracks its
|
|
132
|
+
own dedupe state (last_sent_timestamp) so it doesn't steal frames from the
|
|
133
|
+
SSE broadcaster (get_current_frame_sse), which polls independently.
|
|
131
134
|
"""
|
|
132
135
|
if self.current_frame and self.frame_timestamp > self.last_sent_timestamp:
|
|
133
136
|
self.last_sent_timestamp = self.frame_timestamp
|
|
@@ -135,18 +138,22 @@ class ImageViewerNode(BaseNode):
|
|
|
135
138
|
return None
|
|
136
139
|
|
|
137
140
|
def get_current_frame_sse(self):
|
|
138
|
-
"""SSE handler: return frame wrapped in 'data' key for client compatibility.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
"""SSE handler: return frame wrapped in 'data' key for client compatibility.
|
|
142
|
+
Tracks its own dedupe state (_sse_last_sent) so it doesn't steal frames
|
|
143
|
+
from the polled GET 'frame' route (get_current_frame)."""
|
|
144
|
+
if self.current_frame and self.frame_timestamp > self._sse_last_sent:
|
|
145
|
+
self._sse_last_sent = self.frame_timestamp
|
|
146
|
+
return {'data': self.current_frame}
|
|
142
147
|
return None
|
|
143
148
|
|
|
144
149
|
def get_mjpeg_stream(self):
|
|
145
|
-
"""Generator for MJPEG streaming. Yields (content_type, image_bytes) tuples.
|
|
150
|
+
"""Generator for MJPEG streaming. Yields (content_type, image_bytes) tuples.
|
|
151
|
+
Exits once the node is stopped so the stream doesn't keep a server
|
|
152
|
+
thread alive forever."""
|
|
146
153
|
import time
|
|
147
154
|
import base64
|
|
148
155
|
last_timestamp = 0
|
|
149
|
-
while
|
|
156
|
+
while not self._stop_worker_flag:
|
|
150
157
|
try:
|
|
151
158
|
if self.frame_timestamp > last_timestamp and self.current_frame:
|
|
152
159
|
last_timestamp = self.frame_timestamp
|
|
@@ -23,6 +23,7 @@ Design notes
|
|
|
23
23
|
inventing a parallel notion of "alive".
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
|
+
import copy
|
|
26
27
|
import threading
|
|
27
28
|
|
|
28
29
|
|
|
@@ -68,8 +69,13 @@ class LinkBus:
|
|
|
68
69
|
with self._lock:
|
|
69
70
|
subs = self._subscribers.get(channel)
|
|
70
71
|
targets = list(subs) if subs else []
|
|
72
|
+
# Each subscriber's LinkInNode.send() may take the single-recipient fast
|
|
73
|
+
# path (which shares the payload), so when more than one subscriber
|
|
74
|
+
# receives we hand each its own deepcopy - otherwise every receiving
|
|
75
|
+
# flow would alias one payload. A single subscriber shares (cheap).
|
|
76
|
+
isolate = len(targets) > 1
|
|
71
77
|
for node in targets:
|
|
72
|
-
_deliver(node, msg)
|
|
78
|
+
_deliver(node, copy.deepcopy(msg) if isolate else msg)
|
|
73
79
|
|
|
74
80
|
def subscriber_count(self, channel):
|
|
75
81
|
"""Number of registered subscribers on ``channel`` (for tests/introspection)."""
|
|
@@ -494,7 +494,15 @@ class OmronCameraNode(BaseNode):
|
|
|
494
494
|
# Image conversion
|
|
495
495
|
# ------------------------------------------------------------------
|
|
496
496
|
def _stapi_image_to_numpy(self, st, st_image) -> Optional[np.ndarray]:
|
|
497
|
-
"""Convert a STAPI image to a BGR numpy array.
|
|
497
|
+
"""Convert a STAPI image to a BGR numpy array.
|
|
498
|
+
|
|
499
|
+
The returned array must NOT alias the STAPI acquisition buffer: this is
|
|
500
|
+
called inside the ``retrieve_buffer`` context, and that buffer is
|
|
501
|
+
requeued (and overwritten by future frames) as soon as the context
|
|
502
|
+
exits. ``cv2.cvtColor`` allocates fresh output arrays, but the plain
|
|
503
|
+
``arr.reshape(...)`` paths return a view over ``raw`` - those are copied
|
|
504
|
+
explicitly so a frame handed to ``send()`` owns its pixels (see the
|
|
505
|
+
source-node buffer-reuse rule in base_node's module docstring)."""
|
|
498
506
|
raw = st_image.get_image_data()
|
|
499
507
|
arr = np.frombuffer(raw, dtype=np.uint8)
|
|
500
508
|
h, w = st_image.height, st_image.width
|
|
@@ -521,7 +529,8 @@ class OmronCameraNode(BaseNode):
|
|
|
521
529
|
return cv2.cvtColor(bayer, cv2.COLOR_BayerRG2BGR)
|
|
522
530
|
|
|
523
531
|
elif pix_fmt == st.EStPixelFormatNamingConvention.BGR8:
|
|
524
|
-
|
|
532
|
+
# .copy() so we don't alias the STAPI buffer (requeued on ctx exit).
|
|
533
|
+
return arr.reshape(h, w, 3).copy()
|
|
525
534
|
|
|
526
535
|
elif pix_fmt == st.EStPixelFormatNamingConvention.Mono8:
|
|
527
536
|
gray = arr.reshape(h, w)
|
|
@@ -534,7 +543,8 @@ class OmronCameraNode(BaseNode):
|
|
|
534
543
|
else:
|
|
535
544
|
# Blind fallback: try 3-channel first, then 1-channel → gray2bgr
|
|
536
545
|
try:
|
|
537
|
-
|
|
546
|
+
# .copy() so we don't alias the STAPI buffer (requeued on exit).
|
|
547
|
+
return arr.reshape(h, w, 3).copy()
|
|
538
548
|
except Exception:
|
|
539
549
|
try:
|
|
540
550
|
gray = arr.reshape(h, w)
|
|
@@ -4,6 +4,7 @@ Similar to Node-RED's switch node.
|
|
|
4
4
|
Supports dynamic rules with multiple outputs.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import copy
|
|
7
8
|
import re
|
|
8
9
|
from typing import Any, Dict, List
|
|
9
10
|
from pynode.nodes.base_node import BaseNode, Info, MessageKeys
|
|
@@ -248,9 +249,14 @@ class SwitchNode(BaseNode):
|
|
|
248
249
|
matched_any = False
|
|
249
250
|
for idx, rule in enumerate(rules):
|
|
250
251
|
if self._evaluate_rule(msg_value, rule):
|
|
251
|
-
|
|
252
|
+
# With check_all, the same msg can match several rules and go to
|
|
253
|
+
# multiple outputs. send()'s single-recipient fast path shares
|
|
254
|
+
# the payload, so the first output may share msg but every later
|
|
255
|
+
# output needs an isolated copy (otherwise the branches alias
|
|
256
|
+
# one payload). The common single-match case still shares.
|
|
257
|
+
self.send(msg if not matched_any else copy.deepcopy(msg), idx)
|
|
252
258
|
matched_any = True
|
|
253
|
-
|
|
259
|
+
|
|
254
260
|
# If not checking all rules, stop at first match
|
|
255
261
|
if not check_all:
|
|
256
262
|
break
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# ultralytics_node imports torch at module level. Guard it so lightweight
|
|
2
|
+
# submodules (e.g. model_paths, which has no heavy imports) stay importable in
|
|
3
|
+
# a core-only install without torch/ultralytics. When the vision deps are
|
|
4
|
+
# missing the node is simply unavailable (node auto-discovery already tolerates
|
|
5
|
+
# this via its own ImportError handling).
|
|
6
|
+
try:
|
|
7
|
+
from .ultralytics_node import UltralyticsNode
|
|
8
|
+
__all__ = ['UltralyticsNode']
|
|
9
|
+
except ImportError:
|
|
10
|
+
__all__ = []
|
|
11
|
+
|