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
pynode/api/services.py ADDED
@@ -0,0 +1,124 @@
1
+ """MQTT service management API."""
2
+
3
+ from flask import Blueprint, jsonify
4
+
5
+ from pynode.api.helpers import _INVALID_BODY_ERROR, _get_json_body, _json_error
6
+
7
+ services_bp = Blueprint('services', __name__)
8
+
9
+
10
+ @services_bp.route('/api/services/mqtt', methods=['GET'])
11
+ def list_mqtt_services():
12
+ """List all MQTT services."""
13
+ try:
14
+ from pynode.nodes.MQTTNode.mqtt_service import mqtt_manager
15
+ services = mqtt_manager.list_services()
16
+ return jsonify({'success': True, 'services': services})
17
+ except Exception as e:
18
+ return jsonify({'success': False, 'error': str(e)}), 500
19
+
20
+
21
+ @services_bp.route('/api/services/mqtt', methods=['POST'])
22
+ def create_mqtt_service():
23
+ """Create a new MQTT service."""
24
+ try:
25
+ from pynode.nodes.MQTTNode.mqtt_service import mqtt_manager
26
+ data = _get_json_body()
27
+ if data is None:
28
+ return _json_error(_INVALID_BODY_ERROR, 400)
29
+
30
+ # Validate required fields
31
+ if not data.get('name'):
32
+ return jsonify({'success': False, 'error': 'Service name is required'}), 400
33
+ if not data.get('broker'):
34
+ return jsonify({'success': False, 'error': 'Broker address is required'}), 400
35
+
36
+ service = mqtt_manager.create_service(data)
37
+ return jsonify({
38
+ 'success': True,
39
+ 'service': {
40
+ 'id': service.id,
41
+ 'name': service.name,
42
+ 'broker': service.broker,
43
+ 'port': service.port
44
+ }
45
+ })
46
+ except Exception as e:
47
+ return jsonify({'success': False, 'error': str(e)}), 500
48
+
49
+
50
+ @services_bp.route('/api/services/mqtt/<service_id>', methods=['GET'])
51
+ def get_mqtt_service(service_id):
52
+ """Get a specific MQTT service by ID."""
53
+ try:
54
+ from pynode.nodes.MQTTNode.mqtt_service import mqtt_manager
55
+ service = mqtt_manager.get_service(service_id)
56
+ if not service:
57
+ return jsonify({'success': False, 'error': 'Service not found'}), 404
58
+
59
+ return jsonify({
60
+ 'success': True,
61
+ 'service': service.to_dict()
62
+ })
63
+ except Exception as e:
64
+ return jsonify({'success': False, 'error': str(e)}), 500
65
+
66
+
67
+ @services_bp.route('/api/services/mqtt/<service_id>', methods=['PUT'])
68
+ def update_mqtt_service(service_id):
69
+ """Update an existing MQTT service."""
70
+ try:
71
+ from pynode.nodes.MQTTNode.mqtt_service import mqtt_manager
72
+ data = _get_json_body()
73
+ if data is None:
74
+ return _json_error(_INVALID_BODY_ERROR, 400)
75
+
76
+ service = mqtt_manager.update_service(service_id, data)
77
+ if not service:
78
+ return jsonify({'success': False, 'error': 'Service not found'}), 404
79
+
80
+ return jsonify({
81
+ 'success': True,
82
+ 'service': service.to_dict()
83
+ })
84
+ except Exception as e:
85
+ return jsonify({'success': False, 'error': str(e)}), 500
86
+
87
+
88
+ @services_bp.route('/api/services/mqtt/<service_id>', methods=['DELETE'])
89
+ def delete_mqtt_service(service_id):
90
+ """Delete an MQTT service."""
91
+ try:
92
+ from pynode.nodes.MQTTNode.mqtt_service import mqtt_manager
93
+
94
+ if not mqtt_manager.delete_service(service_id):
95
+ return jsonify({
96
+ 'success': False,
97
+ 'error': 'Service not found or still in use'
98
+ }), 400
99
+
100
+ return jsonify({'success': True})
101
+ except Exception as e:
102
+ return jsonify({'success': False, 'error': str(e)}), 500
103
+
104
+
105
+ @services_bp.route('/api/services/mqtt/<service_id>/test', methods=['POST'])
106
+ def test_mqtt_service(service_id):
107
+ """Test connection to an MQTT service."""
108
+ try:
109
+ from pynode.nodes.MQTTNode.mqtt_service import mqtt_manager
110
+
111
+ service = mqtt_manager.get_service(service_id)
112
+ if not service:
113
+ return jsonify({'success': False, 'error': 'Service not found'}), 404
114
+
115
+ # Try to connect
116
+ connected = service.connect()
117
+
118
+ return jsonify({
119
+ 'success': True,
120
+ 'connected': connected,
121
+ 'status': 'connected' if connected else 'failed'
122
+ })
123
+ except Exception as e:
124
+ return jsonify({'success': False, 'error': str(e)}), 500
pynode/api/sse.py ADDED
@@ -0,0 +1,58 @@
1
+ """Server-Sent Events debug stream.
2
+
3
+ The broadcast worker itself lives on the WorkflowManager
4
+ (``manager.start_debug_broadcast()`` / ``manager.stop_debug_broadcast()``) so
5
+ each app instance has its own worker thread and client queues.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import queue
11
+
12
+ from flask import Blueprint, Response, stream_with_context
13
+
14
+ from pynode.api.helpers import _get_manager
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ sse_bp = Blueprint('sse', __name__)
19
+
20
+
21
+ @sse_bp.route('/api/debug/stream')
22
+ def debug_stream():
23
+ """Server-Sent Events stream for debug messages."""
24
+ manager = _get_manager()
25
+
26
+ def generate():
27
+ # Create a queue for this client
28
+ q = queue.Queue(maxsize=100)
29
+ client_id = id(q)
30
+ with manager.clients_lock:
31
+ manager.debug_message_queues[client_id] = q
32
+
33
+ # Start broadcast thread if not running
34
+ manager.start_debug_broadcast()
35
+
36
+ try:
37
+ yield 'data: {"type": "connected"}\n\n'
38
+
39
+ while True:
40
+ try:
41
+ # Wait for data from broadcast thread
42
+ data = q.get(timeout=1.0)
43
+ yield f'data: {json.dumps(data)}\n\n'
44
+ except queue.Empty:
45
+ # Send keepalive
46
+ yield 'data: {"type": "keepalive"}\n\n'
47
+
48
+ except GeneratorExit:
49
+ # Client disconnected
50
+ with manager.clients_lock:
51
+ manager.debug_message_queues.pop(client_id, None)
52
+ except Exception as e:
53
+ # Log error and close connection
54
+ logger.error(f"SSE Error: {e}")
55
+ with manager.clients_lock:
56
+ manager.debug_message_queues.pop(client_id, None)
57
+
58
+ return Response(stream_with_context(generate()), mimetype='text/event-stream')
pynode/api/uploads.py ADDED
@@ -0,0 +1,59 @@
1
+ """File upload API."""
2
+
3
+ import os
4
+
5
+ from flask import Blueprint, jsonify, request
6
+
7
+ from pynode.api.helpers import _get_manager
8
+
9
+ uploads_bp = Blueprint('uploads', __name__)
10
+
11
+ # File upload configuration: uploads may only land in these subdirectories
12
+ # of the manager's upload_base_dir. Anything else is rejected.
13
+ ALLOWED_UPLOAD_SUBDIRS = ('models', 'uploads')
14
+
15
+
16
+ @uploads_bp.route('/api/upload/file', methods=['POST'])
17
+ def upload_file():
18
+ """Upload a file (model, video, etc.) and save to the server."""
19
+ try:
20
+ if 'file' not in request.files:
21
+ return jsonify({'success': False, 'error': 'No file provided'}), 400
22
+
23
+ file = request.files['file']
24
+ if not file.filename or file.filename == '':
25
+ return jsonify({'success': False, 'error': 'No file selected'}), 400
26
+
27
+ # Determine upload directory from optional 'directory' field, default to models.
28
+ # Only allowlisted subdirectory names are accepted (no separators, no
29
+ # traversal) and the resolved path must stay inside the upload base dir.
30
+ upload_subdir = request.form.get('directory', 'models')
31
+ normalized_subdir = os.path.normpath(upload_subdir).replace('\\', '/')
32
+ if normalized_subdir not in ALLOWED_UPLOAD_SUBDIRS:
33
+ return jsonify({
34
+ 'success': False,
35
+ 'error': f"Invalid upload directory. Allowed: {', '.join(ALLOWED_UPLOAD_SUBDIRS)}"
36
+ }), 400
37
+
38
+ base_dir = os.path.realpath(_get_manager().upload_base_dir)
39
+ upload_dir = os.path.realpath(os.path.join(base_dir, normalized_subdir))
40
+ if upload_dir != base_dir and not upload_dir.startswith(base_dir + os.sep):
41
+ return jsonify({
42
+ 'success': False,
43
+ 'error': 'Invalid upload directory: outside allowed base'
44
+ }), 400
45
+ os.makedirs(upload_dir, exist_ok=True)
46
+
47
+ # Save the file
48
+ filename = os.path.basename(file.filename)
49
+ file_path = os.path.join(upload_dir, filename)
50
+ file.save(file_path)
51
+
52
+ return jsonify({
53
+ 'success': True,
54
+ 'model_path': file_path,
55
+ 'file_path': file_path,
56
+ 'filename': filename
57
+ })
58
+ except Exception as e:
59
+ return jsonify({'success': False, 'error': str(e)}), 500