pynode-flow 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. pynode/__init__.py +37 -0
  2. pynode/__main__.py +9 -0
  3. pynode/_perf.py +65 -0
  4. pynode/_version.py +24 -0
  5. pynode/api/__init__.py +15 -0
  6. pynode/api/helpers.py +79 -0
  7. pynode/api/nodes.py +423 -0
  8. pynode/api/services.py +124 -0
  9. pynode/api/sse.py +58 -0
  10. pynode/api/uploads.py +59 -0
  11. pynode/api/workflows.py +466 -0
  12. pynode/config.py +93 -0
  13. pynode/install_nodes.py +143 -0
  14. pynode/main.py +118 -0
  15. pynode/node_registry.py +156 -0
  16. pynode/nodes/AutoSyncNode/auto_sync_node.py +151 -0
  17. pynode/nodes/BBoxMetricsNode/__init__.py +3 -0
  18. pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +250 -0
  19. pynode/nodes/BBoxMetricsNode/requirements.txt +1 -0
  20. pynode/nodes/BatchNode/__init__.py +3 -0
  21. pynode/nodes/BatchNode/batch_node.py +86 -0
  22. pynode/nodes/BatchNode/requirements.txt +1 -0
  23. pynode/nodes/CameraNode/__init__.py +4 -0
  24. pynode/nodes/CameraNode/camera_node.py +204 -0
  25. pynode/nodes/CameraNode/requirements.txt +2 -0
  26. pynode/nodes/ChangeNode/__init__.py +1 -0
  27. pynode/nodes/ChangeNode/change_node.py +255 -0
  28. pynode/nodes/ChangeNode/requirements.txt +1 -0
  29. pynode/nodes/ConfidenceFilterNode/__init__.py +4 -0
  30. pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +208 -0
  31. pynode/nodes/ConfidenceFilterNode/requirements.txt +1 -0
  32. pynode/nodes/CounterNode/__init__.py +3 -0
  33. pynode/nodes/CounterNode/counter_node.py +191 -0
  34. pynode/nodes/CropNode/__init__.py +4 -0
  35. pynode/nodes/CropNode/crop_node.py +317 -0
  36. pynode/nodes/CropNode/requirements.txt +2 -0
  37. pynode/nodes/DebugNode/__init__.py +4 -0
  38. pynode/nodes/DebugNode/debug_node.py +169 -0
  39. pynode/nodes/DebugNode/requirements.txt +1 -0
  40. pynode/nodes/DelayNode/delay_node.py +218 -0
  41. pynode/nodes/DelayNode/requirements.txt +1 -0
  42. pynode/nodes/DrawPredictionsNode/__init__.py +3 -0
  43. pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +365 -0
  44. pynode/nodes/DrawPredictionsNode/requirements.txt +2 -0
  45. pynode/nodes/ErrorNode/error_node.py +94 -0
  46. pynode/nodes/ErrorNode/requirements.txt +1 -0
  47. pynode/nodes/FilterNode/__init__.py +3 -0
  48. pynode/nodes/FilterNode/filter_node.py +102 -0
  49. pynode/nodes/FilterNode/requirements.txt +1 -0
  50. pynode/nodes/FrameSourceNode/__init__.py +4 -0
  51. pynode/nodes/FrameSourceNode/frame_source_node.py +319 -0
  52. pynode/nodes/FrameSourceNode/requirements.txt +1 -0
  53. pynode/nodes/FunctionNode/__init__.py +3 -0
  54. pynode/nodes/FunctionNode/function_node.py +168 -0
  55. pynode/nodes/FunctionNode/requirements.txt +1 -0
  56. pynode/nodes/GateNode/gate_node.py +60 -0
  57. pynode/nodes/GateNode/requirements.txt +1 -0
  58. pynode/nodes/GetiUploadNode/__init__.py +3 -0
  59. pynode/nodes/GetiUploadNode/geti_upload_node.py +278 -0
  60. pynode/nodes/GetiUploadNode/requirements.txt +1 -0
  61. pynode/nodes/ImageFormatNode/__init__.py +4 -0
  62. pynode/nodes/ImageFormatNode/image_format_node.py +310 -0
  63. pynode/nodes/ImageFormatNode/requirements.txt +2 -0
  64. pynode/nodes/ImageUploadNode/image_upload_node.py +96 -0
  65. pynode/nodes/ImageViewerNode/__init__.py +4 -0
  66. pynode/nodes/ImageViewerNode/image_viewer_node.py +162 -0
  67. pynode/nodes/ImageViewerNode/requirements.txt +2 -0
  68. pynode/nodes/ImageWriterNode/__init__.py +3 -0
  69. pynode/nodes/ImageWriterNode/imagewriter_node.py +352 -0
  70. pynode/nodes/ImageWriterNode/requirements.txt +1 -0
  71. pynode/nodes/InferenceNode/InferenceEngine/__init__.py +15 -0
  72. pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +149 -0
  73. pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +155 -0
  74. pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +272 -0
  75. pynode/nodes/InferenceNode/InferenceEngine/engines/geti_engine.py +309 -0
  76. pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +346 -0
  77. pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +86 -0
  78. pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +863 -0
  79. pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +533 -0
  80. pynode/nodes/InferenceNode/InferenceEngine/readme.md +107 -0
  81. pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +121 -0
  82. pynode/nodes/InferenceNode/inference_node.py +451 -0
  83. pynode/nodes/InferenceNode/requirements.txt +13 -0
  84. pynode/nodes/InjectNode/inject_node.py +245 -0
  85. pynode/nodes/InjectNode/requirements.txt +1 -0
  86. pynode/nodes/JoinNode/__init__.py +3 -0
  87. pynode/nodes/JoinNode/join_node.py +156 -0
  88. pynode/nodes/JoinNode/requirements.txt +1 -0
  89. pynode/nodes/LabelFilterNode/__init__.py +3 -0
  90. pynode/nodes/LabelFilterNode/label_filter_node.py +188 -0
  91. pynode/nodes/LabelFilterNode/requirements.txt +1 -0
  92. pynode/nodes/MQTTNode/__init__.py +8 -0
  93. pynode/nodes/MQTTNode/mqtt_in_node.py +148 -0
  94. pynode/nodes/MQTTNode/mqtt_out_node.py +157 -0
  95. pynode/nodes/MQTTNode/mqtt_service.py +367 -0
  96. pynode/nodes/MQTTNode/requirements.txt +1 -0
  97. pynode/nodes/MessageWriterNode/__init__.py +7 -0
  98. pynode/nodes/MessageWriterNode/messagereader_node.py +568 -0
  99. pynode/nodes/MessageWriterNode/messagewriter_node.py +505 -0
  100. pynode/nodes/OmronCameraNode/__init__.py +3 -0
  101. pynode/nodes/OmronCameraNode/omron_camera_node.py +652 -0
  102. pynode/nodes/OmronCameraNode/requirements.txt +4 -0
  103. pynode/nodes/OpenCV/__init__.py +1 -0
  104. pynode/nodes/OpenCV/arithmetic_node.py +376 -0
  105. pynode/nodes/OpenCV/bitwise_node.py +127 -0
  106. pynode/nodes/OpenCV/blend_node.py +127 -0
  107. pynode/nodes/OpenCV/blob_detector_node.py +233 -0
  108. pynode/nodes/OpenCV/blur_node.py +141 -0
  109. pynode/nodes/OpenCV/color_space_node.py +139 -0
  110. pynode/nodes/OpenCV/colormap_node.py +262 -0
  111. pynode/nodes/OpenCV/contours_node.py +258 -0
  112. pynode/nodes/OpenCV/denormalize_coords_node.py +365 -0
  113. pynode/nodes/OpenCV/draw_node.py +312 -0
  114. pynode/nodes/OpenCV/edge_detector_node.py +177 -0
  115. pynode/nodes/OpenCV/equirectangular_node.py +447 -0
  116. pynode/nodes/OpenCV/fft_node.py +102 -0
  117. pynode/nodes/OpenCV/focus_estimation_node.py +189 -0
  118. pynode/nodes/OpenCV/frequency_filter_node.py +241 -0
  119. pynode/nodes/OpenCV/histogram_node.py +160 -0
  120. pynode/nodes/OpenCV/hough_circles_node.py +224 -0
  121. pynode/nodes/OpenCV/hough_lines_node.py +243 -0
  122. pynode/nodes/OpenCV/in_range_node.py +186 -0
  123. pynode/nodes/OpenCV/morphology_node.py +136 -0
  124. pynode/nodes/OpenCV/normalize_coords_node.py +344 -0
  125. pynode/nodes/OpenCV/paste_node.py +244 -0
  126. pynode/nodes/OpenCV/perspective_node.py +174 -0
  127. pynode/nodes/OpenCV/realsense_depth_node.py +253 -0
  128. pynode/nodes/OpenCV/requirements.txt +2 -0
  129. pynode/nodes/OpenCV/resize_node.py +175 -0
  130. pynode/nodes/OpenCV/rotate_node.py +159 -0
  131. pynode/nodes/OpenCV/template_match_node.py +227 -0
  132. pynode/nodes/OpenCV/threshold_node.py +167 -0
  133. pynode/nodes/PointInShapeNode/__init__.py +3 -0
  134. pynode/nodes/PointInShapeNode/point_in_shape_node.py +313 -0
  135. pynode/nodes/PointInShapeNode/requirements.txt +2 -0
  136. pynode/nodes/PolygonMetricsNode/__init__.py +3 -0
  137. pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +331 -0
  138. pynode/nodes/PolygonMetricsNode/requirements.txt +2 -0
  139. pynode/nodes/QueueLengthProbeNode/__init__.py +3 -0
  140. pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +116 -0
  141. pynode/nodes/Qwen3VLMNode/__init__.py +3 -0
  142. pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +519 -0
  143. pynode/nodes/Qwen3VLMNode/requirements.txt +5 -0
  144. pynode/nodes/RESTEndpointNode/__init__.py +3 -0
  145. pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +491 -0
  146. pynode/nodes/RangeNode/__init__.py +3 -0
  147. pynode/nodes/RangeNode/range_node.py +115 -0
  148. pynode/nodes/RangeNode/requirements.txt +1 -0
  149. pynode/nodes/RateProbeNode/__init__.py +3 -0
  150. pynode/nodes/RateProbeNode/rate_probe_node.py +183 -0
  151. pynode/nodes/RateProbeNode/requirements.txt +1 -0
  152. pynode/nodes/RoboflowUploadNode/__init__.py +3 -0
  153. pynode/nodes/RoboflowUploadNode/requirements.txt +1 -0
  154. pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +229 -0
  155. pynode/nodes/SliceNode/README.md +149 -0
  156. pynode/nodes/SliceNode/__init__.py +5 -0
  157. pynode/nodes/SliceNode/merge_predictions_node.py +444 -0
  158. pynode/nodes/SliceNode/requirements.txt +3 -0
  159. pynode/nodes/SliceNode/slice_collector_node.py +370 -0
  160. pynode/nodes/SliceNode/slice_image_node.py +380 -0
  161. pynode/nodes/SplitNode/__init__.py +3 -0
  162. pynode/nodes/SplitNode/requirements.txt +1 -0
  163. pynode/nodes/SplitNode/split_node.py +155 -0
  164. pynode/nodes/SwitchNode/requirements.txt +1 -0
  165. pynode/nodes/SwitchNode/switch_node.py +259 -0
  166. pynode/nodes/SyncNode/__init__.py +3 -0
  167. pynode/nodes/SyncNode/sync_node.py +287 -0
  168. pynode/nodes/TemplateNode/requirements.txt +1 -0
  169. pynode/nodes/TemplateNode/template_node.py +86 -0
  170. pynode/nodes/TrackerNode/__init__.py +2 -0
  171. pynode/nodes/TrackerNode/supervision_tracker_node.py +248 -0
  172. pynode/nodes/TrackerNode/tracker_node.py +745 -0
  173. pynode/nodes/UltralyticsNode/__init__.py +4 -0
  174. pynode/nodes/UltralyticsNode/requirements.txt +2 -0
  175. pynode/nodes/UltralyticsNode/ultralytics_node.py +364 -0
  176. pynode/nodes/UnknownNode/__init__.py +93 -0
  177. pynode/nodes/VLLMNode/__init__.py +3 -0
  178. pynode/nodes/VLLMNode/vllm_node.py +345 -0
  179. pynode/nodes/VideoReaderNode/__init__.py +3 -0
  180. pynode/nodes/VideoReaderNode/video_reader_node.py +425 -0
  181. pynode/nodes/VideoWriterNode/__init__.py +3 -0
  182. pynode/nodes/VideoWriterNode/video_writer_node.py +441 -0
  183. pynode/nodes/WebhookNode/__init__.py +3 -0
  184. pynode/nodes/WebhookNode/webhook_node.py +591 -0
  185. pynode/nodes/__init__.py +109 -0
  186. pynode/nodes/base_node.py +562 -0
  187. pynode/nodes/image_utils.py +291 -0
  188. pynode/nodes/info.py +109 -0
  189. pynode/nodes/mDNSNode/__init__.py +13 -0
  190. pynode/nodes/mDNSNode/mdns_broadcast_node.py +214 -0
  191. pynode/nodes/mDNSNode/mdns_discovery_node.py +362 -0
  192. pynode/nodes/mDNSNode/mdns_manager.py +399 -0
  193. pynode/nodes/mDNSNode/requirements.txt +1 -0
  194. pynode/nodes/messages.py +81 -0
  195. pynode/server.py +187 -0
  196. pynode/static/images/favicon4.png +0 -0
  197. pynode/static/index.html +200 -0
  198. pynode/static/js/README.md +139 -0
  199. pynode/static/js/auth.js +89 -0
  200. pynode/static/js/clipboard.js +152 -0
  201. pynode/static/js/config.js +14 -0
  202. pynode/static/js/connections.js +844 -0
  203. pynode/static/js/debug.js +524 -0
  204. pynode/static/js/events.js +775 -0
  205. pynode/static/js/history.js +199 -0
  206. pynode/static/js/main.js +55 -0
  207. pynode/static/js/minimap.js +242 -0
  208. pynode/static/js/nodes.js +995 -0
  209. pynode/static/js/palette.js +255 -0
  210. pynode/static/js/properties.js +1270 -0
  211. pynode/static/js/selection.js +241 -0
  212. pynode/static/js/state.js +211 -0
  213. pynode/static/js/ui-utils.js +28 -0
  214. pynode/static/js/viewport.js +204 -0
  215. pynode/static/js/workflow.js +469 -0
  216. pynode/static/js/workflows.js +344 -0
  217. pynode/static/style.css +2751 -0
  218. pynode/workflow_engine.py +483 -0
  219. pynode/workflow_manager.py +440 -0
  220. pynode_flow-0.2.0.dist-info/METADATA +520 -0
  221. pynode_flow-0.2.0.dist-info/RECORD +227 -0
  222. pynode_flow-0.2.0.dist-info/WHEEL +5 -0
  223. pynode_flow-0.2.0.dist-info/entry_points.txt +3 -0
  224. pynode_flow-0.2.0.dist-info/licenses/LICENSE +21 -0
  225. pynode_flow-0.2.0.dist-info/scm_file_list.json +270 -0
  226. pynode_flow-0.2.0.dist-info/scm_version.json +8 -0
  227. pynode_flow-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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)