pynode-flow 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pynode/__init__.py +37 -0
- pynode/__main__.py +9 -0
- pynode/_perf.py +65 -0
- pynode/_version.py +24 -0
- pynode/api/__init__.py +15 -0
- pynode/api/helpers.py +79 -0
- pynode/api/nodes.py +423 -0
- pynode/api/services.py +124 -0
- pynode/api/sse.py +58 -0
- pynode/api/uploads.py +59 -0
- pynode/api/workflows.py +466 -0
- pynode/config.py +93 -0
- pynode/install_nodes.py +143 -0
- pynode/main.py +118 -0
- pynode/node_registry.py +156 -0
- pynode/nodes/AutoSyncNode/auto_sync_node.py +151 -0
- pynode/nodes/BBoxMetricsNode/__init__.py +3 -0
- pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +250 -0
- pynode/nodes/BBoxMetricsNode/requirements.txt +1 -0
- pynode/nodes/BatchNode/__init__.py +3 -0
- pynode/nodes/BatchNode/batch_node.py +86 -0
- pynode/nodes/BatchNode/requirements.txt +1 -0
- pynode/nodes/CameraNode/__init__.py +4 -0
- pynode/nodes/CameraNode/camera_node.py +204 -0
- pynode/nodes/CameraNode/requirements.txt +2 -0
- pynode/nodes/ChangeNode/__init__.py +1 -0
- pynode/nodes/ChangeNode/change_node.py +255 -0
- pynode/nodes/ChangeNode/requirements.txt +1 -0
- pynode/nodes/ConfidenceFilterNode/__init__.py +4 -0
- pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +208 -0
- pynode/nodes/ConfidenceFilterNode/requirements.txt +1 -0
- pynode/nodes/CounterNode/__init__.py +3 -0
- pynode/nodes/CounterNode/counter_node.py +191 -0
- pynode/nodes/CropNode/__init__.py +4 -0
- pynode/nodes/CropNode/crop_node.py +317 -0
- pynode/nodes/CropNode/requirements.txt +2 -0
- pynode/nodes/DebugNode/__init__.py +4 -0
- pynode/nodes/DebugNode/debug_node.py +169 -0
- pynode/nodes/DebugNode/requirements.txt +1 -0
- pynode/nodes/DelayNode/delay_node.py +218 -0
- pynode/nodes/DelayNode/requirements.txt +1 -0
- pynode/nodes/DrawPredictionsNode/__init__.py +3 -0
- pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +365 -0
- pynode/nodes/DrawPredictionsNode/requirements.txt +2 -0
- pynode/nodes/ErrorNode/error_node.py +94 -0
- pynode/nodes/ErrorNode/requirements.txt +1 -0
- pynode/nodes/FilterNode/__init__.py +3 -0
- pynode/nodes/FilterNode/filter_node.py +102 -0
- pynode/nodes/FilterNode/requirements.txt +1 -0
- pynode/nodes/FrameSourceNode/__init__.py +4 -0
- pynode/nodes/FrameSourceNode/frame_source_node.py +319 -0
- pynode/nodes/FrameSourceNode/requirements.txt +1 -0
- pynode/nodes/FunctionNode/__init__.py +3 -0
- pynode/nodes/FunctionNode/function_node.py +168 -0
- pynode/nodes/FunctionNode/requirements.txt +1 -0
- pynode/nodes/GateNode/gate_node.py +60 -0
- pynode/nodes/GateNode/requirements.txt +1 -0
- pynode/nodes/GetiUploadNode/__init__.py +3 -0
- pynode/nodes/GetiUploadNode/geti_upload_node.py +278 -0
- pynode/nodes/GetiUploadNode/requirements.txt +1 -0
- pynode/nodes/ImageFormatNode/__init__.py +4 -0
- pynode/nodes/ImageFormatNode/image_format_node.py +310 -0
- pynode/nodes/ImageFormatNode/requirements.txt +2 -0
- pynode/nodes/ImageUploadNode/image_upload_node.py +96 -0
- pynode/nodes/ImageViewerNode/__init__.py +4 -0
- pynode/nodes/ImageViewerNode/image_viewer_node.py +162 -0
- pynode/nodes/ImageViewerNode/requirements.txt +2 -0
- pynode/nodes/ImageWriterNode/__init__.py +3 -0
- pynode/nodes/ImageWriterNode/imagewriter_node.py +352 -0
- pynode/nodes/ImageWriterNode/requirements.txt +1 -0
- pynode/nodes/InferenceNode/InferenceEngine/__init__.py +15 -0
- pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +149 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +155 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +272 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/geti_engine.py +309 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +346 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +86 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +863 -0
- pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +533 -0
- pynode/nodes/InferenceNode/InferenceEngine/readme.md +107 -0
- pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +121 -0
- pynode/nodes/InferenceNode/inference_node.py +451 -0
- pynode/nodes/InferenceNode/requirements.txt +13 -0
- pynode/nodes/InjectNode/inject_node.py +245 -0
- pynode/nodes/InjectNode/requirements.txt +1 -0
- pynode/nodes/JoinNode/__init__.py +3 -0
- pynode/nodes/JoinNode/join_node.py +156 -0
- pynode/nodes/JoinNode/requirements.txt +1 -0
- pynode/nodes/LabelFilterNode/__init__.py +3 -0
- pynode/nodes/LabelFilterNode/label_filter_node.py +188 -0
- pynode/nodes/LabelFilterNode/requirements.txt +1 -0
- pynode/nodes/MQTTNode/__init__.py +8 -0
- pynode/nodes/MQTTNode/mqtt_in_node.py +148 -0
- pynode/nodes/MQTTNode/mqtt_out_node.py +157 -0
- pynode/nodes/MQTTNode/mqtt_service.py +367 -0
- pynode/nodes/MQTTNode/requirements.txt +1 -0
- pynode/nodes/MessageWriterNode/__init__.py +7 -0
- pynode/nodes/MessageWriterNode/messagereader_node.py +568 -0
- pynode/nodes/MessageWriterNode/messagewriter_node.py +505 -0
- pynode/nodes/OmronCameraNode/__init__.py +3 -0
- pynode/nodes/OmronCameraNode/omron_camera_node.py +652 -0
- pynode/nodes/OmronCameraNode/requirements.txt +4 -0
- pynode/nodes/OpenCV/__init__.py +1 -0
- pynode/nodes/OpenCV/arithmetic_node.py +376 -0
- pynode/nodes/OpenCV/bitwise_node.py +127 -0
- pynode/nodes/OpenCV/blend_node.py +127 -0
- pynode/nodes/OpenCV/blob_detector_node.py +233 -0
- pynode/nodes/OpenCV/blur_node.py +141 -0
- pynode/nodes/OpenCV/color_space_node.py +139 -0
- pynode/nodes/OpenCV/colormap_node.py +262 -0
- pynode/nodes/OpenCV/contours_node.py +258 -0
- pynode/nodes/OpenCV/denormalize_coords_node.py +365 -0
- pynode/nodes/OpenCV/draw_node.py +312 -0
- pynode/nodes/OpenCV/edge_detector_node.py +177 -0
- pynode/nodes/OpenCV/equirectangular_node.py +447 -0
- pynode/nodes/OpenCV/fft_node.py +102 -0
- pynode/nodes/OpenCV/focus_estimation_node.py +189 -0
- pynode/nodes/OpenCV/frequency_filter_node.py +241 -0
- pynode/nodes/OpenCV/histogram_node.py +160 -0
- pynode/nodes/OpenCV/hough_circles_node.py +224 -0
- pynode/nodes/OpenCV/hough_lines_node.py +243 -0
- pynode/nodes/OpenCV/in_range_node.py +186 -0
- pynode/nodes/OpenCV/morphology_node.py +136 -0
- pynode/nodes/OpenCV/normalize_coords_node.py +344 -0
- pynode/nodes/OpenCV/paste_node.py +244 -0
- pynode/nodes/OpenCV/perspective_node.py +174 -0
- pynode/nodes/OpenCV/realsense_depth_node.py +253 -0
- pynode/nodes/OpenCV/requirements.txt +2 -0
- pynode/nodes/OpenCV/resize_node.py +175 -0
- pynode/nodes/OpenCV/rotate_node.py +159 -0
- pynode/nodes/OpenCV/template_match_node.py +227 -0
- pynode/nodes/OpenCV/threshold_node.py +167 -0
- pynode/nodes/PointInShapeNode/__init__.py +3 -0
- pynode/nodes/PointInShapeNode/point_in_shape_node.py +313 -0
- pynode/nodes/PointInShapeNode/requirements.txt +2 -0
- pynode/nodes/PolygonMetricsNode/__init__.py +3 -0
- pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +331 -0
- pynode/nodes/PolygonMetricsNode/requirements.txt +2 -0
- pynode/nodes/QueueLengthProbeNode/__init__.py +3 -0
- pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +116 -0
- pynode/nodes/Qwen3VLMNode/__init__.py +3 -0
- pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +519 -0
- pynode/nodes/Qwen3VLMNode/requirements.txt +5 -0
- pynode/nodes/RESTEndpointNode/__init__.py +3 -0
- pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +491 -0
- pynode/nodes/RangeNode/__init__.py +3 -0
- pynode/nodes/RangeNode/range_node.py +115 -0
- pynode/nodes/RangeNode/requirements.txt +1 -0
- pynode/nodes/RateProbeNode/__init__.py +3 -0
- pynode/nodes/RateProbeNode/rate_probe_node.py +183 -0
- pynode/nodes/RateProbeNode/requirements.txt +1 -0
- pynode/nodes/RoboflowUploadNode/__init__.py +3 -0
- pynode/nodes/RoboflowUploadNode/requirements.txt +1 -0
- pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +229 -0
- pynode/nodes/SliceNode/README.md +149 -0
- pynode/nodes/SliceNode/__init__.py +5 -0
- pynode/nodes/SliceNode/merge_predictions_node.py +444 -0
- pynode/nodes/SliceNode/requirements.txt +3 -0
- pynode/nodes/SliceNode/slice_collector_node.py +370 -0
- pynode/nodes/SliceNode/slice_image_node.py +380 -0
- pynode/nodes/SplitNode/__init__.py +3 -0
- pynode/nodes/SplitNode/requirements.txt +1 -0
- pynode/nodes/SplitNode/split_node.py +155 -0
- pynode/nodes/SwitchNode/requirements.txt +1 -0
- pynode/nodes/SwitchNode/switch_node.py +259 -0
- pynode/nodes/SyncNode/__init__.py +3 -0
- pynode/nodes/SyncNode/sync_node.py +287 -0
- pynode/nodes/TemplateNode/requirements.txt +1 -0
- pynode/nodes/TemplateNode/template_node.py +86 -0
- pynode/nodes/TrackerNode/__init__.py +2 -0
- pynode/nodes/TrackerNode/supervision_tracker_node.py +248 -0
- pynode/nodes/TrackerNode/tracker_node.py +745 -0
- pynode/nodes/UltralyticsNode/__init__.py +4 -0
- pynode/nodes/UltralyticsNode/requirements.txt +2 -0
- pynode/nodes/UltralyticsNode/ultralytics_node.py +364 -0
- pynode/nodes/UnknownNode/__init__.py +93 -0
- pynode/nodes/VLLMNode/__init__.py +3 -0
- pynode/nodes/VLLMNode/vllm_node.py +345 -0
- pynode/nodes/VideoReaderNode/__init__.py +3 -0
- pynode/nodes/VideoReaderNode/video_reader_node.py +425 -0
- pynode/nodes/VideoWriterNode/__init__.py +3 -0
- pynode/nodes/VideoWriterNode/video_writer_node.py +441 -0
- pynode/nodes/WebhookNode/__init__.py +3 -0
- pynode/nodes/WebhookNode/webhook_node.py +591 -0
- pynode/nodes/__init__.py +109 -0
- pynode/nodes/base_node.py +562 -0
- pynode/nodes/image_utils.py +291 -0
- pynode/nodes/info.py +109 -0
- pynode/nodes/mDNSNode/__init__.py +13 -0
- pynode/nodes/mDNSNode/mdns_broadcast_node.py +214 -0
- pynode/nodes/mDNSNode/mdns_discovery_node.py +362 -0
- pynode/nodes/mDNSNode/mdns_manager.py +399 -0
- pynode/nodes/mDNSNode/requirements.txt +1 -0
- pynode/nodes/messages.py +81 -0
- pynode/server.py +187 -0
- pynode/static/images/favicon4.png +0 -0
- pynode/static/index.html +200 -0
- pynode/static/js/README.md +139 -0
- pynode/static/js/auth.js +89 -0
- pynode/static/js/clipboard.js +152 -0
- pynode/static/js/config.js +14 -0
- pynode/static/js/connections.js +844 -0
- pynode/static/js/debug.js +524 -0
- pynode/static/js/events.js +775 -0
- pynode/static/js/history.js +199 -0
- pynode/static/js/main.js +55 -0
- pynode/static/js/minimap.js +242 -0
- pynode/static/js/nodes.js +995 -0
- pynode/static/js/palette.js +255 -0
- pynode/static/js/properties.js +1270 -0
- pynode/static/js/selection.js +241 -0
- pynode/static/js/state.js +211 -0
- pynode/static/js/ui-utils.js +28 -0
- pynode/static/js/viewport.js +204 -0
- pynode/static/js/workflow.js +469 -0
- pynode/static/js/workflows.js +344 -0
- pynode/static/style.css +2751 -0
- pynode/workflow_engine.py +483 -0
- pynode/workflow_manager.py +440 -0
- pynode_flow-0.2.0.dist-info/METADATA +520 -0
- pynode_flow-0.2.0.dist-info/RECORD +227 -0
- pynode_flow-0.2.0.dist-info/WHEEL +5 -0
- pynode_flow-0.2.0.dist-info/entry_points.txt +3 -0
- pynode_flow-0.2.0.dist-info/licenses/LICENSE +21 -0
- pynode_flow-0.2.0.dist-info/scm_file_list.json +270 -0
- pynode_flow-0.2.0.dist-info/scm_version.json +8 -0
- pynode_flow-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Factory module for creating inference engines
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import logging
|
|
9
|
+
import importlib
|
|
10
|
+
import inspect
|
|
11
|
+
|
|
12
|
+
from flask import json
|
|
13
|
+
|
|
14
|
+
# Handle both standalone and module imports
|
|
15
|
+
try:
|
|
16
|
+
from .engines.base_engine import BaseInferenceEngine
|
|
17
|
+
except ImportError:
|
|
18
|
+
# If running as standalone script, add current directory to path
|
|
19
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
20
|
+
parent_dir = os.path.dirname(current_dir)
|
|
21
|
+
if parent_dir not in sys.path:
|
|
22
|
+
sys.path.insert(0, parent_dir)
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
from InferenceEngine.engines.base_engine import BaseInferenceEngine
|
|
26
|
+
except ImportError:
|
|
27
|
+
# Last resort: direct local imports
|
|
28
|
+
sys.path.insert(0, current_dir)
|
|
29
|
+
from engines.base_engine import BaseInferenceEngine
|
|
30
|
+
|
|
31
|
+
# Configure logging
|
|
32
|
+
logging.basicConfig(level=logging.INFO)
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class InferenceEngineFactory:
|
|
37
|
+
"""Factory class for creating inference engine instances."""
|
|
38
|
+
|
|
39
|
+
# Start with empty dictionaries - will be populated by auto-discovery
|
|
40
|
+
_engine_types = {}
|
|
41
|
+
_engine_display_names = {}
|
|
42
|
+
_discovery_complete = False
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def _discover_engines(cls):
|
|
46
|
+
"""Automatically discover and register engine classes from the engines folder"""
|
|
47
|
+
if cls._discovery_complete:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
logger.info("Starting automatic engine discovery...")
|
|
51
|
+
|
|
52
|
+
# Get the engines directory path
|
|
53
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
54
|
+
engines_dir = os.path.join(current_dir, 'engines')
|
|
55
|
+
|
|
56
|
+
if not os.path.exists(engines_dir):
|
|
57
|
+
logger.warning(f"Engines directory not found: {engines_dir}")
|
|
58
|
+
cls._discovery_complete = True
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
# Files to skip during discovery
|
|
62
|
+
skip_files = {
|
|
63
|
+
'__init__.py',
|
|
64
|
+
'base_engine.py',
|
|
65
|
+
'example_engine_template.py',
|
|
66
|
+
'__pycache__'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Scan for Python files in the engines directory
|
|
70
|
+
for filename in os.listdir(engines_dir):
|
|
71
|
+
if filename in skip_files:
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
if filename.endswith('.py'):
|
|
75
|
+
module_name = filename[:-3] # Remove .py extension
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
# Import the module
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
# Running as standalone script
|
|
81
|
+
sys.path.insert(0, engines_dir)
|
|
82
|
+
module = importlib.import_module(module_name)
|
|
83
|
+
else:
|
|
84
|
+
# Running as package - try multiple import strategies
|
|
85
|
+
module = None
|
|
86
|
+
import_errors = []
|
|
87
|
+
|
|
88
|
+
# Strategy 1: Use __package__ to build the full path dynamically
|
|
89
|
+
# __package__ gives us something like 'nodes.InferenceNode.InferenceEngine'
|
|
90
|
+
if __package__:
|
|
91
|
+
try:
|
|
92
|
+
module = importlib.import_module(f'{__package__}.engines.{module_name}')
|
|
93
|
+
except ImportError as e:
|
|
94
|
+
import_errors.append(f"Package path ({__package__}): {e}")
|
|
95
|
+
|
|
96
|
+
# Strategy 2: Relative import from current package
|
|
97
|
+
if module is None:
|
|
98
|
+
try:
|
|
99
|
+
module = importlib.import_module(f'.engines.{module_name}', package=__package__)
|
|
100
|
+
except ImportError as e:
|
|
101
|
+
import_errors.append(f"Relative with __package__: {e}")
|
|
102
|
+
|
|
103
|
+
# Strategy 3: Direct import after adding to sys.path
|
|
104
|
+
if module is None:
|
|
105
|
+
try:
|
|
106
|
+
if engines_dir not in sys.path:
|
|
107
|
+
sys.path.insert(0, engines_dir)
|
|
108
|
+
module = importlib.import_module(module_name)
|
|
109
|
+
except ImportError as e:
|
|
110
|
+
import_errors.append(f"Direct: {e}")
|
|
111
|
+
|
|
112
|
+
if module is None:
|
|
113
|
+
logger.warning(f"Failed to import {module_name}: {import_errors}")
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
# Find engine classes in the module
|
|
117
|
+
for name, obj in inspect.getmembers(module, inspect.isclass):
|
|
118
|
+
# Check if it's a valid engine class
|
|
119
|
+
if (obj != BaseInferenceEngine and
|
|
120
|
+
issubclass(obj, BaseInferenceEngine) and
|
|
121
|
+
obj.__module__ == module.__name__):
|
|
122
|
+
|
|
123
|
+
# Generate engine key from class name
|
|
124
|
+
engine_key = cls._class_name_to_key(name)
|
|
125
|
+
|
|
126
|
+
# Register the engine
|
|
127
|
+
if engine_key not in cls._engine_types:
|
|
128
|
+
cls._engine_types[engine_key] = obj
|
|
129
|
+
|
|
130
|
+
# Set display name
|
|
131
|
+
if hasattr(obj, 'display_name'):
|
|
132
|
+
cls._engine_display_names[engine_key] = obj.display_name
|
|
133
|
+
else:
|
|
134
|
+
cls._engine_display_names[engine_key] = engine_key.replace('_', ' ').title()
|
|
135
|
+
|
|
136
|
+
logger.info(f"Auto-discovered engine: {engine_key} -> {obj.__name__} ({cls._engine_display_names[engine_key]})")
|
|
137
|
+
else:
|
|
138
|
+
logger.debug(f"Engine key '{engine_key}' already registered, skipping {obj.__name__}")
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.warning(f"Failed to import engine from {filename}: {e}")
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
cls._discovery_complete = True
|
|
145
|
+
logger.info(f"Engine discovery complete. Found {len(cls._engine_types)} engines.")
|
|
146
|
+
|
|
147
|
+
@classmethod
|
|
148
|
+
def _class_name_to_key(cls, class_name: str) -> str:
|
|
149
|
+
"""
|
|
150
|
+
Convert a class name to an engine key.
|
|
151
|
+
|
|
152
|
+
Examples:
|
|
153
|
+
- UltralyticsEngine -> ultralytics
|
|
154
|
+
- GetiEngine -> geti
|
|
155
|
+
- CustomObjectDetectionEngine -> custom_object_detection
|
|
156
|
+
- MyAIEngine -> my_ai
|
|
157
|
+
"""
|
|
158
|
+
# Remove common suffixes
|
|
159
|
+
suffixes_to_remove = ['Engine', 'Inference', 'AI', 'Model']
|
|
160
|
+
key = class_name
|
|
161
|
+
|
|
162
|
+
for suffix in suffixes_to_remove:
|
|
163
|
+
if key.endswith(suffix) and len(key) > len(suffix):
|
|
164
|
+
key = key[:-len(suffix)]
|
|
165
|
+
break
|
|
166
|
+
|
|
167
|
+
# Convert CamelCase to snake_case
|
|
168
|
+
result = []
|
|
169
|
+
for i, char in enumerate(key):
|
|
170
|
+
if char.isupper() and i > 0:
|
|
171
|
+
result.append('_')
|
|
172
|
+
result.append(char.lower())
|
|
173
|
+
|
|
174
|
+
return ''.join(result)
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def _initialize_display_names(cls):
|
|
178
|
+
"""Initialize display names from engine classes if not already set"""
|
|
179
|
+
cls._discover_engines() # Ensure discovery is complete
|
|
180
|
+
|
|
181
|
+
for engine_type, engine_class in cls._engine_types.items():
|
|
182
|
+
if engine_type not in cls._engine_display_names:
|
|
183
|
+
if hasattr(engine_class, 'display_name'):
|
|
184
|
+
cls._engine_display_names[engine_type] = engine_class.display_name
|
|
185
|
+
else:
|
|
186
|
+
cls._engine_display_names[engine_type] = engine_type.replace('_', ' ').title()
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def create(cls, engine_type=None, **kwargs):
|
|
190
|
+
"""
|
|
191
|
+
Create an inference engine instance.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
engine_type: Type of engine ('ultralytics', 'geti', etc.)
|
|
195
|
+
**kwargs: Additional parameters for the specific engine type
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
BaseInferenceEngine: Configured engine instance
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
ValueError: If engine_type is not supported
|
|
202
|
+
"""
|
|
203
|
+
cls._discover_engines() # Ensure engines are discovered
|
|
204
|
+
|
|
205
|
+
if engine_type is None:
|
|
206
|
+
engine_type = kwargs.pop('engine_type', None)
|
|
207
|
+
|
|
208
|
+
if engine_type not in cls._engine_types:
|
|
209
|
+
available_types = ', '.join(cls._engine_types.keys())
|
|
210
|
+
raise ValueError(f"Unsupported engine type: {engine_type}. Available types: {available_types}")
|
|
211
|
+
|
|
212
|
+
engine_class = cls._engine_types[engine_type]
|
|
213
|
+
return engine_class(**kwargs)
|
|
214
|
+
|
|
215
|
+
@classmethod
|
|
216
|
+
def register_engine(cls, name: str, engine_class: type, display_name: str | None = None):
|
|
217
|
+
"""
|
|
218
|
+
Register a new engine type manually.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
name: Name of the engine type
|
|
222
|
+
engine_class: Class implementing BaseInferenceEngine
|
|
223
|
+
display_name: Optional user-friendly display name
|
|
224
|
+
"""
|
|
225
|
+
if not issubclass(engine_class, BaseInferenceEngine):
|
|
226
|
+
logger.warning(f"Engine class {engine_class} does not inherit from BaseInferenceEngine")
|
|
227
|
+
|
|
228
|
+
if name in cls._engine_types:
|
|
229
|
+
logger.warning(f"Engine type '{name}' already registered, replacing with new class.")
|
|
230
|
+
|
|
231
|
+
cls._engine_types[name] = engine_class
|
|
232
|
+
|
|
233
|
+
# Set display name (priority: provided name > engine class display_name > fallback)
|
|
234
|
+
if display_name:
|
|
235
|
+
cls._engine_display_names[name] = display_name
|
|
236
|
+
elif hasattr(engine_class, 'display_name'):
|
|
237
|
+
cls._engine_display_names[name] = engine_class.display_name
|
|
238
|
+
elif name not in cls._engine_display_names:
|
|
239
|
+
cls._engine_display_names[name] = name.replace('_', ' ').title()
|
|
240
|
+
|
|
241
|
+
logger.info(f"Manually registered engine type: {name} ({cls._engine_display_names[name]})")
|
|
242
|
+
|
|
243
|
+
@classmethod
|
|
244
|
+
def get_available_types(cls) -> list:
|
|
245
|
+
"""Get list of available engine types."""
|
|
246
|
+
cls._discover_engines() # Ensure engines are discovered
|
|
247
|
+
return list(cls._engine_types.keys())
|
|
248
|
+
|
|
249
|
+
@classmethod
|
|
250
|
+
def get_display_name(cls, engine_type: str) -> str:
|
|
251
|
+
"""
|
|
252
|
+
Get the user-friendly display name for an engine type.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
engine_type: The engine type key
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
str: User-friendly display name
|
|
259
|
+
"""
|
|
260
|
+
cls._initialize_display_names()
|
|
261
|
+
|
|
262
|
+
# First check if we have a custom display name in our mapping
|
|
263
|
+
if engine_type in cls._engine_display_names:
|
|
264
|
+
return cls._engine_display_names[engine_type]
|
|
265
|
+
|
|
266
|
+
# If engine class exists, try to get display name from the engine class itself
|
|
267
|
+
if engine_type in cls._engine_types:
|
|
268
|
+
engine_class = cls._engine_types[engine_type]
|
|
269
|
+
if hasattr(engine_class, 'display_name'):
|
|
270
|
+
return engine_class.display_name
|
|
271
|
+
|
|
272
|
+
# Fallback to formatted engine type
|
|
273
|
+
return engine_type.replace('_', ' ').title()
|
|
274
|
+
|
|
275
|
+
@classmethod
|
|
276
|
+
def get_available_engines_with_names(cls) -> dict:
|
|
277
|
+
"""
|
|
278
|
+
Get a dictionary mapping engine types to their display names.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
dict: {engine_type: display_name}
|
|
282
|
+
"""
|
|
283
|
+
cls._initialize_display_names()
|
|
284
|
+
return {
|
|
285
|
+
engine_type: cls.get_display_name(engine_type)
|
|
286
|
+
for engine_type in cls._engine_types.keys()
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
@classmethod
|
|
290
|
+
def get_available_engines_with_metadata(cls) -> list:
|
|
291
|
+
"""
|
|
292
|
+
Get a list of available inference engines with detailed metadata.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
list: List of dictionaries containing engine metadata
|
|
296
|
+
"""
|
|
297
|
+
cls._discover_engines() # Ensure engines are discovered
|
|
298
|
+
|
|
299
|
+
available_engines = []
|
|
300
|
+
|
|
301
|
+
# Define metadata for each engine type
|
|
302
|
+
engine_metadata = {
|
|
303
|
+
'ultralytics': {
|
|
304
|
+
'icon': 'fas fa-robot',
|
|
305
|
+
'description': 'YOLO object detection and segmentation models',
|
|
306
|
+
'primary': True,
|
|
307
|
+
'dependencies': ['ultralytics', 'torch', 'torchvision']
|
|
308
|
+
},
|
|
309
|
+
'geti': {
|
|
310
|
+
'icon': 'fas fa-microchip',
|
|
311
|
+
'description': 'Geti computer vision platform',
|
|
312
|
+
'primary': True,
|
|
313
|
+
'dependencies': ['geti-sdk']
|
|
314
|
+
},
|
|
315
|
+
'pass': {
|
|
316
|
+
'icon': 'fas fa-forward',
|
|
317
|
+
'description': 'Pass-through engine for testing',
|
|
318
|
+
'primary': False,
|
|
319
|
+
'dependencies': []
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
for engine_type in cls._engine_types.keys():
|
|
324
|
+
engine_class = cls._engine_types[engine_type]
|
|
325
|
+
|
|
326
|
+
# Get basic metadata
|
|
327
|
+
metadata = engine_metadata.get(engine_type, {
|
|
328
|
+
'icon': 'fas fa-cog',
|
|
329
|
+
'description': f'{cls.get_display_name(engine_type)} inference engine',
|
|
330
|
+
'primary': False,
|
|
331
|
+
'dependencies': []
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
# Check availability by testing dependencies
|
|
335
|
+
available = True
|
|
336
|
+
error_message = None
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
# Try to instantiate the engine to check if dependencies are available
|
|
340
|
+
test_engine = engine_class()
|
|
341
|
+
if hasattr(test_engine, 'check_dependencies'):
|
|
342
|
+
available = test_engine.check_dependencies()
|
|
343
|
+
if not available:
|
|
344
|
+
error_message = "Dependencies not available"
|
|
345
|
+
except ImportError as e:
|
|
346
|
+
available = False
|
|
347
|
+
error_message = f"Missing dependencies: {str(e)}"
|
|
348
|
+
except Exception as e:
|
|
349
|
+
available = False
|
|
350
|
+
error_message = f"Engine not available: {str(e)}"
|
|
351
|
+
|
|
352
|
+
engine_info = {
|
|
353
|
+
'type': engine_type,
|
|
354
|
+
'name': cls.get_display_name(engine_type),
|
|
355
|
+
'icon': metadata['icon'],
|
|
356
|
+
'description': metadata['description'],
|
|
357
|
+
'available': available,
|
|
358
|
+
'primary': metadata.get('primary', False),
|
|
359
|
+
'dependencies': metadata.get('dependencies', [])
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if not available and error_message:
|
|
363
|
+
engine_info['error'] = error_message
|
|
364
|
+
|
|
365
|
+
available_engines.append(engine_info)
|
|
366
|
+
|
|
367
|
+
# Sort: primary engines first, then by name
|
|
368
|
+
available_engines.sort(key=lambda x: (not x['primary'], x['name']))
|
|
369
|
+
|
|
370
|
+
return available_engines
|
|
371
|
+
|
|
372
|
+
@classmethod
|
|
373
|
+
def unregister_engine(cls, engine_type: str):
|
|
374
|
+
"""Unregister an engine type (convenience function)"""
|
|
375
|
+
if engine_type not in cls._engine_types:
|
|
376
|
+
raise ValueError(f"Engine type '{engine_type}' is not registered")
|
|
377
|
+
del cls._engine_types[engine_type]
|
|
378
|
+
|
|
379
|
+
# Also remove from display names if present
|
|
380
|
+
if engine_type in cls._engine_display_names:
|
|
381
|
+
del cls._engine_display_names[engine_type]
|
|
382
|
+
|
|
383
|
+
logger.info(f"Unregistered engine type: {engine_type}")
|
|
384
|
+
|
|
385
|
+
@classmethod
|
|
386
|
+
def rediscover_engines(cls):
|
|
387
|
+
"""Force re-discovery of engines (useful for development)"""
|
|
388
|
+
cls._discovery_complete = False
|
|
389
|
+
cls._engine_types.clear()
|
|
390
|
+
cls._engine_display_names.clear()
|
|
391
|
+
cls._discover_engines()
|
|
392
|
+
logger.info("Forced engine re-discovery completed")
|
|
393
|
+
|
|
394
|
+
@classmethod
|
|
395
|
+
def get_discovery_info(cls) -> dict:
|
|
396
|
+
"""Get information about the discovery process"""
|
|
397
|
+
cls._discover_engines() # Ensure discovery is complete
|
|
398
|
+
return {
|
|
399
|
+
"discovery_complete": cls._discovery_complete,
|
|
400
|
+
"engines_found": len(cls._engine_types),
|
|
401
|
+
"engine_types": list(cls._engine_types.keys()),
|
|
402
|
+
"engines_with_display_names": cls._engine_display_names.copy()
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
if __name__ == "__main__":
|
|
408
|
+
# Test the factory function
|
|
409
|
+
import logging
|
|
410
|
+
logging.basicConfig(level=logging.INFO)
|
|
411
|
+
logger = logging.getLogger(__name__)
|
|
412
|
+
|
|
413
|
+
logger.info("Testing InferenceEngine factory with auto-discovery...")
|
|
414
|
+
|
|
415
|
+
# Test discovery info
|
|
416
|
+
discovery_info = InferenceEngineFactory.get_discovery_info()
|
|
417
|
+
logger.info(f"Discovery info: {discovery_info}")
|
|
418
|
+
|
|
419
|
+
# Test listing engines
|
|
420
|
+
logger.info(f"Available engines: {InferenceEngineFactory.get_available_types()}")
|
|
421
|
+
logger.info(f"Engine display names: {InferenceEngineFactory.get_available_engines_with_names()}")
|
|
422
|
+
|
|
423
|
+
# Test individual display names
|
|
424
|
+
for engine_type in InferenceEngineFactory.get_available_types():
|
|
425
|
+
display_name = InferenceEngineFactory.get_display_name(engine_type)
|
|
426
|
+
logger.info(f" {engine_type} -> {display_name}")
|
|
427
|
+
|
|
428
|
+
# Test all discovered engines
|
|
429
|
+
for engine_type in InferenceEngineFactory.get_available_types():
|
|
430
|
+
try:
|
|
431
|
+
engine = InferenceEngineFactory.create(engine_type)
|
|
432
|
+
logger.info(f"✓ Successfully created {engine_type} engine: {type(engine)}")
|
|
433
|
+
|
|
434
|
+
# Test that it's properly instantiated
|
|
435
|
+
info = engine.get_info()
|
|
436
|
+
logger.info(f" Engine info: {info}")
|
|
437
|
+
|
|
438
|
+
except Exception as e:
|
|
439
|
+
logger.error(f"✗ Failed to create {engine_type} engine: {e}")
|
|
440
|
+
|
|
441
|
+
# Test runtime registration with a mock engine
|
|
442
|
+
class MockEngine(BaseInferenceEngine):
|
|
443
|
+
"""Mock engine for testing registration"""
|
|
444
|
+
display_name = "Mock Test Engine"
|
|
445
|
+
|
|
446
|
+
def __init__(self, **kwargs):
|
|
447
|
+
super().__init__(**kwargs)
|
|
448
|
+
self.name = "MockEngine"
|
|
449
|
+
|
|
450
|
+
def _load_model(self, model_file: str, device: str) -> bool:
|
|
451
|
+
return True
|
|
452
|
+
|
|
453
|
+
def check_valid_model(self, model_file: str) -> bool:
|
|
454
|
+
return True
|
|
455
|
+
|
|
456
|
+
def _preprocess(self, image):
|
|
457
|
+
return image
|
|
458
|
+
|
|
459
|
+
def _infer(self, preprocessed_input):
|
|
460
|
+
return {"mock": "result"}
|
|
461
|
+
|
|
462
|
+
def _postprocess(self, raw_output):
|
|
463
|
+
return raw_output
|
|
464
|
+
|
|
465
|
+
def draw(self, image, results):
|
|
466
|
+
return image
|
|
467
|
+
|
|
468
|
+
def result_to_json(self, results, output_format: str = "dict") -> str:
|
|
469
|
+
if output_format == "dict":
|
|
470
|
+
return json.dumps(results)
|
|
471
|
+
return '{"mock": "result"}'
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
# Test manual registration with display name
|
|
475
|
+
InferenceEngineFactory.register_engine('mock', MockEngine, 'Override Mock Engine')
|
|
476
|
+
logger.info("✓ Successfully registered mock engine with custom display name")
|
|
477
|
+
|
|
478
|
+
# Test display name
|
|
479
|
+
display_name = InferenceEngineFactory.get_display_name('mock')
|
|
480
|
+
logger.info(f" Mock engine display name: {display_name}")
|
|
481
|
+
|
|
482
|
+
# Test creation of registered engine
|
|
483
|
+
mock_engine = InferenceEngineFactory.create('mock')
|
|
484
|
+
logger.info(f"✓ Successfully created mock engine: {type(mock_engine)}")
|
|
485
|
+
logger.info(f" Mock engine info: {mock_engine.get_info()}")
|
|
486
|
+
|
|
487
|
+
# Test updated engine list
|
|
488
|
+
logger.info(f"Updated available engines: {InferenceEngineFactory.get_available_types()}")
|
|
489
|
+
|
|
490
|
+
# Test unregistration
|
|
491
|
+
InferenceEngineFactory.unregister_engine('mock')
|
|
492
|
+
logger.info("✓ Successfully unregistered mock engine")
|
|
493
|
+
logger.info(f"Final available engines: {InferenceEngineFactory.get_available_types()}")
|
|
494
|
+
|
|
495
|
+
except Exception as e:
|
|
496
|
+
logger.error(f"✗ Failed runtime registration test: {e}")
|
|
497
|
+
|
|
498
|
+
# Test invalid engine type
|
|
499
|
+
try:
|
|
500
|
+
engine = InferenceEngineFactory.create('invalid_engine')
|
|
501
|
+
logger.error("✗ Should have raised ValueError for invalid engine")
|
|
502
|
+
except ValueError as e:
|
|
503
|
+
logger.info(f"✓ Correctly raised ValueError for invalid engine: {e}")
|
|
504
|
+
except Exception as e:
|
|
505
|
+
logger.error(f"✗ Unexpected error for invalid engine: {e}")
|
|
506
|
+
|
|
507
|
+
# Test rediscovery
|
|
508
|
+
try:
|
|
509
|
+
logger.info("Testing rediscovery...")
|
|
510
|
+
initial_count = len(InferenceEngineFactory.get_available_types())
|
|
511
|
+
InferenceEngineFactory.rediscover_engines()
|
|
512
|
+
final_count = len(InferenceEngineFactory.get_available_types())
|
|
513
|
+
logger.info(f"✓ Rediscovery completed: {initial_count} -> {final_count} engines")
|
|
514
|
+
|
|
515
|
+
except Exception as e:
|
|
516
|
+
logger.error(f"✗ Rediscovery test failed: {e}")
|
|
517
|
+
|
|
518
|
+
# Test class name to key conversion
|
|
519
|
+
test_names = [
|
|
520
|
+
"UltralyticsEngine",
|
|
521
|
+
"GetiEngine",
|
|
522
|
+
"CustomObjectDetectionEngine",
|
|
523
|
+
"MyAIEngine",
|
|
524
|
+
"SimpleEngine",
|
|
525
|
+
"AdvancedInferenceEngine"
|
|
526
|
+
]
|
|
527
|
+
|
|
528
|
+
logger.info("Testing class name to key conversion:")
|
|
529
|
+
for name in test_names:
|
|
530
|
+
key = InferenceEngineFactory._class_name_to_key(name)
|
|
531
|
+
logger.info(f" {name} -> {key}")
|
|
532
|
+
|
|
533
|
+
logger.info("Factory testing completed!")
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# InferenceEngine Factory - Runtime Registration
|
|
2
|
+
|
|
3
|
+
The InferenceEngine factory now supports runtime registration of custom engines, allowing you to add new inference engines without modifying the core factory code.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Runtime Registration**: Register new engines at runtime
|
|
8
|
+
- **Dynamic Engine Management**: List, register, and unregister engines
|
|
9
|
+
- **Backward Compatibility**: Existing factory function interface unchanged
|
|
10
|
+
- **Validation**: Basic validation of engine classes during registration
|
|
11
|
+
|
|
12
|
+
## Basic Usage
|
|
13
|
+
|
|
14
|
+
### Using Built-in Engines
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from InferenceEngine.inference_engine_factory import InferenceEngineFactory
|
|
18
|
+
|
|
19
|
+
# Create built-in engines
|
|
20
|
+
engine = InferenceEngineFactory('ultralytics')
|
|
21
|
+
geti_engine = InferenceEngineFactory('geti')
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Runtime Registration
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from InferenceEngine.inference_engine_factory import register_engine, InferenceEngineFactory, list_engines
|
|
28
|
+
|
|
29
|
+
# Define your custom engine
|
|
30
|
+
class MyCustomEngine:
|
|
31
|
+
def __init__(self):
|
|
32
|
+
self.model = None
|
|
33
|
+
|
|
34
|
+
def get_info(self):
|
|
35
|
+
return {"engine": "custom", "version": "1.0"}
|
|
36
|
+
|
|
37
|
+
def load(self, model_path, device='cpu'):
|
|
38
|
+
# Your model loading logic
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
def infer(self, input_data):
|
|
42
|
+
# Your inference logic
|
|
43
|
+
return {"predictions": []}
|
|
44
|
+
|
|
45
|
+
# Register the engine
|
|
46
|
+
register_engine('my_custom', MyCustomEngine)
|
|
47
|
+
|
|
48
|
+
# Use the registered engine
|
|
49
|
+
engine = InferenceEngineFactory('my_custom')
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## API Reference
|
|
53
|
+
|
|
54
|
+
### Functions
|
|
55
|
+
|
|
56
|
+
- `InferenceEngineFactory(engine_type: str)` - Create engine instance (backward compatible)
|
|
57
|
+
- `register_engine(engine_type: str, engine_class)` - Register new engine type
|
|
58
|
+
- `unregister_engine(engine_type: str)` - Remove engine type
|
|
59
|
+
- `list_engines()` - Get list of available engine types
|
|
60
|
+
|
|
61
|
+
### InferenceEngineRegistry Class
|
|
62
|
+
|
|
63
|
+
Direct access to the registry for advanced usage:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from InferenceEngine.inference_engine_factory import _engine_registry
|
|
67
|
+
|
|
68
|
+
# Access registry directly
|
|
69
|
+
_engine_registry.register_engine('advanced', AdvancedEngine)
|
|
70
|
+
engine = _engine_registry.create_engine('advanced')
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Engine Requirements
|
|
74
|
+
|
|
75
|
+
Custom engines should implement these methods:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
class CustomEngine:
|
|
79
|
+
def __init__(self):
|
|
80
|
+
"""Initialize the engine"""
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
def get_info(self):
|
|
84
|
+
"""Return engine information dict"""
|
|
85
|
+
return {"engine": "name", "version": "1.0"}
|
|
86
|
+
|
|
87
|
+
def load(self, model_path, device='cpu'):
|
|
88
|
+
"""Load model, return True if successful"""
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
def infer(self, input_data):
|
|
92
|
+
"""Run inference, return results dict"""
|
|
93
|
+
return {"predictions": []}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Example
|
|
97
|
+
|
|
98
|
+
See `example_custom_engine.py` for complete examples of:
|
|
99
|
+
- TensorFlow custom engine
|
|
100
|
+
- OpenVINO custom engine
|
|
101
|
+
- Registration and usage patterns
|
|
102
|
+
|
|
103
|
+
## Error Handling
|
|
104
|
+
|
|
105
|
+
- `ValueError` - Raised for invalid engine types or duplicate registrations
|
|
106
|
+
- Runtime validation ensures engine classes are callable
|
|
107
|
+
- Optional inheritance checking from BaseInferenceEngine (if available)
|