nedo-vision-worker-core 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.

Potentially problematic release.


This version of nedo-vision-worker-core might be problematic. Click here for more details.

Files changed (95) hide show
  1. nedo_vision_worker_core/__init__.py +23 -0
  2. nedo_vision_worker_core/ai/FrameDrawer.py +144 -0
  3. nedo_vision_worker_core/ai/ImageDebugger.py +126 -0
  4. nedo_vision_worker_core/ai/VideoDebugger.py +69 -0
  5. nedo_vision_worker_core/ai/__init__.py +1 -0
  6. nedo_vision_worker_core/cli.py +197 -0
  7. nedo_vision_worker_core/config/ConfigurationManager.py +173 -0
  8. nedo_vision_worker_core/config/__init__.py +1 -0
  9. nedo_vision_worker_core/core_service.py +237 -0
  10. nedo_vision_worker_core/database/DatabaseManager.py +236 -0
  11. nedo_vision_worker_core/database/__init__.py +1 -0
  12. nedo_vision_worker_core/detection/BaseDetector.py +22 -0
  13. nedo_vision_worker_core/detection/DetectionManager.py +83 -0
  14. nedo_vision_worker_core/detection/RFDETRDetector.py +62 -0
  15. nedo_vision_worker_core/detection/YOLODetector.py +57 -0
  16. nedo_vision_worker_core/detection/__init__.py +1 -0
  17. nedo_vision_worker_core/detection/detection_processing/DetectionProcessor.py +29 -0
  18. nedo_vision_worker_core/detection/detection_processing/HumanDetectionProcessor.py +47 -0
  19. nedo_vision_worker_core/detection/detection_processing/PPEDetectionProcessor.py +44 -0
  20. nedo_vision_worker_core/detection/detection_processing/__init__.py +1 -0
  21. nedo_vision_worker_core/doctor.py +342 -0
  22. nedo_vision_worker_core/drawing_assets/blue/inner_corner.png +0 -0
  23. nedo_vision_worker_core/drawing_assets/blue/inner_frame.png +0 -0
  24. nedo_vision_worker_core/drawing_assets/blue/line.png +0 -0
  25. nedo_vision_worker_core/drawing_assets/blue/top_left.png +0 -0
  26. nedo_vision_worker_core/drawing_assets/blue/top_right.png +0 -0
  27. nedo_vision_worker_core/drawing_assets/red/inner_corner.png +0 -0
  28. nedo_vision_worker_core/drawing_assets/red/inner_frame.png +0 -0
  29. nedo_vision_worker_core/drawing_assets/red/line.png +0 -0
  30. nedo_vision_worker_core/drawing_assets/red/top_left.png +0 -0
  31. nedo_vision_worker_core/drawing_assets/red/top_right.png +0 -0
  32. nedo_vision_worker_core/icons/boots-green.png +0 -0
  33. nedo_vision_worker_core/icons/boots-red.png +0 -0
  34. nedo_vision_worker_core/icons/gloves-green.png +0 -0
  35. nedo_vision_worker_core/icons/gloves-red.png +0 -0
  36. nedo_vision_worker_core/icons/goggles-green.png +0 -0
  37. nedo_vision_worker_core/icons/goggles-red.png +0 -0
  38. nedo_vision_worker_core/icons/helmet-green.png +0 -0
  39. nedo_vision_worker_core/icons/helmet-red.png +0 -0
  40. nedo_vision_worker_core/icons/mask-red.png +0 -0
  41. nedo_vision_worker_core/icons/vest-green.png +0 -0
  42. nedo_vision_worker_core/icons/vest-red.png +0 -0
  43. nedo_vision_worker_core/models/__init__.py +20 -0
  44. nedo_vision_worker_core/models/ai_model.py +41 -0
  45. nedo_vision_worker_core/models/auth.py +14 -0
  46. nedo_vision_worker_core/models/config.py +9 -0
  47. nedo_vision_worker_core/models/dataset_source.py +30 -0
  48. nedo_vision_worker_core/models/logs.py +9 -0
  49. nedo_vision_worker_core/models/ppe_detection.py +39 -0
  50. nedo_vision_worker_core/models/ppe_detection_label.py +20 -0
  51. nedo_vision_worker_core/models/restricted_area_violation.py +20 -0
  52. nedo_vision_worker_core/models/user.py +10 -0
  53. nedo_vision_worker_core/models/worker_source.py +19 -0
  54. nedo_vision_worker_core/models/worker_source_pipeline.py +21 -0
  55. nedo_vision_worker_core/models/worker_source_pipeline_config.py +24 -0
  56. nedo_vision_worker_core/models/worker_source_pipeline_debug.py +15 -0
  57. nedo_vision_worker_core/models/worker_source_pipeline_detection.py +14 -0
  58. nedo_vision_worker_core/pipeline/PipelineConfigManager.py +32 -0
  59. nedo_vision_worker_core/pipeline/PipelineManager.py +133 -0
  60. nedo_vision_worker_core/pipeline/PipelinePrepocessor.py +40 -0
  61. nedo_vision_worker_core/pipeline/PipelineProcessor.py +338 -0
  62. nedo_vision_worker_core/pipeline/PipelineSyncThread.py +202 -0
  63. nedo_vision_worker_core/pipeline/__init__.py +1 -0
  64. nedo_vision_worker_core/preprocessing/ImageResizer.py +42 -0
  65. nedo_vision_worker_core/preprocessing/ImageRoi.py +61 -0
  66. nedo_vision_worker_core/preprocessing/Preprocessor.py +16 -0
  67. nedo_vision_worker_core/preprocessing/__init__.py +1 -0
  68. nedo_vision_worker_core/repositories/AIModelRepository.py +31 -0
  69. nedo_vision_worker_core/repositories/PPEDetectionRepository.py +146 -0
  70. nedo_vision_worker_core/repositories/RestrictedAreaRepository.py +90 -0
  71. nedo_vision_worker_core/repositories/WorkerSourcePipelineDebugRepository.py +81 -0
  72. nedo_vision_worker_core/repositories/WorkerSourcePipelineDetectionRepository.py +71 -0
  73. nedo_vision_worker_core/repositories/WorkerSourcePipelineRepository.py +79 -0
  74. nedo_vision_worker_core/repositories/WorkerSourceRepository.py +19 -0
  75. nedo_vision_worker_core/repositories/__init__.py +1 -0
  76. nedo_vision_worker_core/streams/RTMPStreamer.py +146 -0
  77. nedo_vision_worker_core/streams/StreamSyncThread.py +66 -0
  78. nedo_vision_worker_core/streams/VideoStream.py +324 -0
  79. nedo_vision_worker_core/streams/VideoStreamManager.py +121 -0
  80. nedo_vision_worker_core/streams/__init__.py +1 -0
  81. nedo_vision_worker_core/tracker/SFSORT.py +325 -0
  82. nedo_vision_worker_core/tracker/TrackerManager.py +163 -0
  83. nedo_vision_worker_core/tracker/__init__.py +1 -0
  84. nedo_vision_worker_core/util/BoundingBoxMetrics.py +53 -0
  85. nedo_vision_worker_core/util/DrawingUtils.py +354 -0
  86. nedo_vision_worker_core/util/ModelReadinessChecker.py +188 -0
  87. nedo_vision_worker_core/util/PersonAttributeMatcher.py +70 -0
  88. nedo_vision_worker_core/util/PersonRestrictedAreaMatcher.py +45 -0
  89. nedo_vision_worker_core/util/TablePrinter.py +28 -0
  90. nedo_vision_worker_core/util/__init__.py +1 -0
  91. nedo_vision_worker_core-0.2.0.dist-info/METADATA +347 -0
  92. nedo_vision_worker_core-0.2.0.dist-info/RECORD +95 -0
  93. nedo_vision_worker_core-0.2.0.dist-info/WHEEL +5 -0
  94. nedo_vision_worker_core-0.2.0.dist-info/entry_points.txt +2 -0
  95. nedo_vision_worker_core-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,23 @@
1
+ """
2
+ Nedo Vision Core Library
3
+
4
+ A library for running AI vision processing and detection in the Nedo Vision platform.
5
+ """
6
+
7
+ from .core_service import CoreService
8
+
9
+ __version__ = "0.2.0"
10
+ __all__ = ["CoreService"]
11
+
12
+ # Convenience functions for callback management
13
+ def register_ppe_detection_callback(callback):
14
+ """Register a callback for PPE detection events."""
15
+ return CoreService.register_detection_callback('ppe_detection', callback)
16
+
17
+ def register_area_violation_callback(callback):
18
+ """Register a callback for restricted area violation events."""
19
+ return CoreService.register_detection_callback('area_violation', callback)
20
+
21
+ def register_general_detection_callback(callback):
22
+ """Register a callback for all detection events."""
23
+ return CoreService.register_detection_callback('general_detection', callback)
@@ -0,0 +1,144 @@
1
+ from typing import List, Tuple
2
+ import cv2
3
+ import logging
4
+ import os
5
+ from pathlib import Path
6
+
7
+ import numpy as np
8
+ from ..util.DrawingUtils import DrawingUtils
9
+
10
+ class FrameDrawer:
11
+ """Handles frame processing by drawing objects, annotations, and managing icons."""
12
+
13
+ def __init__(self):
14
+ self.icons = {}
15
+ self.violation_labels = []
16
+ self.compliance_labels = []
17
+ self.polygons: List[Tuple[Tuple[int, int, int], List[Tuple[float, float]]]] = []
18
+ self.trails = {}
19
+ self.location_name = "LOCATION"
20
+
21
+ def update_config(self, icons = {}, violation_labels = [], compliance_labels = []):
22
+ self.icons = self._load_icons(icons)
23
+ self.violation_labels = violation_labels
24
+ self.compliance_labels = compliance_labels
25
+
26
+ def _load_icons(self, icon_paths, size=(20, 20)):
27
+ icons = {}
28
+ base_dir = Path(__file__).parent.parent
29
+
30
+ for key, path in icon_paths.items():
31
+ try:
32
+ full_path = base_dir / path
33
+ icon = cv2.imread(str(full_path), cv2.IMREAD_UNCHANGED)
34
+ if icon is None:
35
+ raise FileNotFoundError(f"Icon '{path}' not found at {full_path}.")
36
+ icons[key] = cv2.resize(icon, size)
37
+ except Exception as e:
38
+ logging.warning(f"⚠️ Failed to load icon {path}: {e}")
39
+ icons[key] = None
40
+ return icons
41
+
42
+ def _get_color(self, label):
43
+ if label in self.violation_labels:
44
+ return ((0, 0, 255), False)
45
+ elif label in self.compliance_labels:
46
+ return ((255, 141, 11), True)
47
+ else:
48
+ return ((255, 255, 255), None)
49
+
50
+ def _get_color_from_labels(self, labels):
51
+ compliance_count = 0
52
+ for label in labels:
53
+ if label in self.violation_labels:
54
+ return ((0, 0, 255), False)
55
+ elif label in self.compliance_labels:
56
+ compliance_count += 1
57
+
58
+ if labels and compliance_count == len(labels):
59
+ return ((255, 141, 11), True)
60
+ else:
61
+ return ((255, 255, 255), None)
62
+
63
+ def draw_polygons(self, frame):
64
+ height, width = frame.shape[:2]
65
+ for color, normalized_points in self.polygons:
66
+ points = [
67
+ (int(x * width), int(y * height)) for (x, y) in normalized_points
68
+ ]
69
+ if len(points) >= 3:
70
+ cv2.polylines(frame, [np.array(points, np.int32)], isClosed=True, color=color, thickness=2)
71
+
72
+ def draw_frame(self, frame, tracked_objects, with_trails=False, trail_length=10):
73
+ current_ids = set()
74
+
75
+ for obj in tracked_objects:
76
+ bbox = obj.get("bbox", [])
77
+ if len(bbox) != 4:
78
+ continue
79
+
80
+ track_id = obj.get('track_id', -1)
81
+ x1, y1, x2, y2 = map(int, bbox)
82
+
83
+ attributes = obj.get("attributes", [])
84
+ labels = [attr.get("label") for attr in attributes]
85
+ (color, flag) = self._get_color_from_labels(labels)
86
+ DrawingUtils.draw_bbox_info(frame, bbox, (color, flag), f"{track_id}", self.location_name, f"{obj.get('confidence', 0):.2f}")
87
+
88
+ # Trailing
89
+ if with_trails:
90
+ if not hasattr(self, "trails"):
91
+ self.trails = {}
92
+
93
+ if track_id not in self.trails:
94
+ self.trails[track_id] = {
95
+ "points": [],
96
+ "missed_frames": 0
97
+ }
98
+
99
+ center_x = int((x1 + x2) / 2)
100
+ center_y = int((y1 + y2) / 2)
101
+
102
+ self.trails[track_id]["points"].append((center_x, center_y))
103
+
104
+ # Limit the number of points
105
+ if len(self.trails[track_id]["points"]) > trail_length:
106
+ self.trails[track_id]["points"] = self.trails[track_id]["points"][-trail_length:]
107
+
108
+ points = self.trails[track_id]["points"]
109
+ num_points = len(points)
110
+
111
+ # Draw faded trail
112
+ for i in range(1, num_points):
113
+ cv2.line(frame, points[i-1], points[i], color, 1)
114
+
115
+ current_ids.add(track_id)
116
+
117
+ DrawingUtils.draw_main_bbox(frame, bbox, (color, flag))
118
+
119
+ for attr in attributes:
120
+ attr_bbox = attr.get("bbox", [])
121
+ if len(attr_bbox) != 4:
122
+ continue
123
+
124
+ attr_label = attr.get("label", "")
125
+ DrawingUtils.draw_inner_box(frame, attr_bbox, self._get_color(attr_label))
126
+
127
+ icon_x = x1
128
+ for (label, icon) in self.icons.items():
129
+ if label in labels:
130
+ DrawingUtils.draw_alpha_overlay(frame, icon, icon_x, y1 - 25)
131
+ icon_x += 25
132
+
133
+ # Cleanup trails for objects that disappeared
134
+ if with_trails and hasattr(self, "trails"):
135
+ for track_id in list(self.trails.keys()):
136
+ if track_id not in current_ids:
137
+ self.trails[track_id]["missed_frames"] += 1
138
+ else:
139
+ self.trails[track_id]["missed_frames"] = 0
140
+
141
+ if self.trails[track_id]["missed_frames"] > 30:
142
+ del self.trails[track_id]
143
+
144
+ return frame
@@ -0,0 +1,126 @@
1
+ import cv2
2
+ import random
3
+ import logging
4
+ import threading
5
+ import os
6
+
7
+ class ImageDebugger:
8
+ """Handles image visualization for AI detections and tracking."""
9
+ def __init__(self, window_name="AI Debugger", display=True):
10
+ """
11
+ Initializes the ImageDebugger.
12
+
13
+ :param window_name: Name of the display window.
14
+ :param display: Flag to enable visualization (skips if no GUI available).
15
+ """
16
+ self.window_name = window_name
17
+ self.display = display and os.environ.get("DISPLAY", None) is not None
18
+
19
+ def draw_detections(self, frame, detections):
20
+ """
21
+ Draws bounding boxes and labels on the given frame.
22
+
23
+ :param frame: The image frame to draw on.
24
+ :param detections: List of detection results.
25
+ :return: Image with drawn detections.
26
+ """
27
+ for detection in detections:
28
+ label = detection.get("label", "unknown")
29
+ confidence = detection.get("confidence", 0)
30
+ bbox = detection.get("bbox", [])
31
+
32
+ if not bbox or len(bbox) != 4:
33
+ continue # Skip invalid bounding boxes
34
+
35
+ color = self._get_color(label)
36
+ x1, y1, x2, y2 = map(int, bbox)
37
+
38
+ # Draw bounding box
39
+ cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
40
+
41
+ # Draw label and confidence
42
+ label_text = f"{label} ({confidence:.2f})"
43
+ cv2.putText(frame, label_text, (x1, max(y1 - 10, 20)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
44
+
45
+ return frame
46
+
47
+ def draw_tracked_objects(self, frame, tracked_objects):
48
+ """
49
+ Draws tracked objects with unique IDs and attribute matches.
50
+
51
+ :param frame: The image frame to draw on.
52
+ :param tracked_objects: List of tracked object results.
53
+ :return: Image with drawn tracking details.
54
+ """
55
+ for obj in tracked_objects:
56
+ track_id = obj.get("track_id", -1)
57
+ detections = obj.get("detections", 0)
58
+ bbox = obj.get("bbox", [])
59
+
60
+ if not bbox or len(bbox) != 4:
61
+ continue # Skip invalid bounding boxes
62
+
63
+ color = self._get_color("track") # Unique color for tracked objects
64
+ x1, y1, x2, y2 = map(int, bbox)
65
+
66
+ # Draw tracking box
67
+ cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
68
+
69
+ # Draw track ID and detection count
70
+ track_text = f"ID: {track_id} | Detections: {detections}"
71
+ cv2.putText(frame, track_text, (x1, max(y1 - 10, 20)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
72
+
73
+ # Draw attributes
74
+ for attr in obj.get("attributes", []):
75
+ attr_label = attr.get("label", "unknown")
76
+ attr_conf = attr.get("confidence", 0)
77
+ attr_bbox = attr.get("bbox", [])
78
+
79
+ if attr_bbox and len(attr_bbox) == 4:
80
+ ax1, ay1, ax2, ay2 = map(int, attr_bbox)
81
+ attr_color = self._get_color(attr_label)
82
+ cv2.rectangle(frame, (ax1, ay1), (ax2, ay2), attr_color, 2)
83
+ cv2.putText(frame, f"{attr_label} ({attr_conf:.2f})", (ax1, max(ay1 - 10, 20)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, attr_color, 2)
84
+
85
+ return frame
86
+
87
+ def show_image(self, frame):
88
+ """Displays the image in a window without blocking execution."""
89
+ if self.display:
90
+ print("[DEBUG] Showing image...") # Debug print
91
+ cv2.imshow(self.window_name, frame)
92
+ key = cv2.waitKey(10) & 0xFF
93
+ if key == ord('q'): # Allows exit with 'q'
94
+ self.close()
95
+ print("[DEBUG] Image displayed.")
96
+
97
+
98
+ def show_image_threaded(self, frame):
99
+ """Runs show_image in a separate thread to prevent blocking."""
100
+ thread = threading.Thread(target=self.show_image, args=(frame,))
101
+ thread.start()
102
+
103
+ def save_image(self, frame, filename="debug_output.jpg"):
104
+ """
105
+ Saves the image to a file.
106
+
107
+ :param frame: Image to save.
108
+ :param filename: Name of the output file.
109
+ """
110
+ cv2.imwrite(filename, frame)
111
+ logging.info(f"📸 Debug image saved as {filename}")
112
+
113
+ def close(self):
114
+ """Closes all OpenCV windows."""
115
+ cv2.destroyAllWindows()
116
+ cv2.waitKey(1) # Ensure the window fully closes
117
+
118
+ def _get_color(self, label):
119
+ """
120
+ Generates a unique color for each label.
121
+
122
+ :param label: Label name.
123
+ :return: BGR color tuple.
124
+ """
125
+ random.seed(hash(label) % 256)
126
+ return tuple(random.randint(100, 255) for _ in range(3))
@@ -0,0 +1,69 @@
1
+ import cv2
2
+ import threading
3
+ import time
4
+ from collections import defaultdict
5
+
6
+ # TODO: fix timer error (because of threading)
7
+ class VideoDebugger:
8
+ """Handles real-time visualization of video streams with object detections."""
9
+
10
+ def __init__(self, enable_visualization=True):
11
+ """
12
+ Initializes the VideoDebugger with frame drawing and visualization capabilities.
13
+
14
+ Args:
15
+ enable_visualization (bool): Whether to display frames.
16
+ """
17
+ self.enable_visualization = enable_visualization
18
+ self.windows = {} # Tracks OpenCV windows
19
+ self.lock = threading.Lock() # Thread-safe updates
20
+ self.fps_tracker = defaultdict(lambda: {"start_time": time.time(), "frame_count": 0})
21
+
22
+ def show_frame(self, pipeline_id, worker_source_id, frame):
23
+ """
24
+ Displays a frame with FPS overlay.
25
+
26
+ Args:
27
+ pipeline_id (str/int): Identifier for the pipeline.
28
+ worker_source_id (str): Identifier for the worker/source.
29
+ frame: The frame to display.
30
+ """
31
+ if not self.enable_visualization or frame is None:
32
+ return
33
+
34
+ window_name = f"Pipeline {pipeline_id} - {worker_source_id}"
35
+ with self.lock:
36
+ if window_name not in self.fps_tracker:
37
+ self.fps_tracker[window_name] = {"start_time": time.time(), "frame_count": 0}
38
+
39
+ self.fps_tracker[window_name]["frame_count"] += 1
40
+ elapsed_time = time.time() - self.fps_tracker[window_name]["start_time"]
41
+ fps = self.fps_tracker[window_name]["frame_count"] / max(elapsed_time, 1e-5)
42
+
43
+ # Display FPS on the frame
44
+ cv2.putText(frame, f"FPS: {fps:.2f}", (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 255), 1)
45
+
46
+ # Show the window
47
+ if window_name not in self.windows:
48
+ self.windows[window_name] = True # Register window
49
+
50
+ cv2.imshow(window_name, frame)
51
+
52
+ # Close on 'q' press
53
+ if cv2.waitKey(1) & 0xFF == ord('q'):
54
+ self.close_window(window_name)
55
+
56
+ def close_window(self, window_name):
57
+ """Closes a specific OpenCV window."""
58
+ with self.lock:
59
+ if window_name in self.windows:
60
+ cv2.destroyWindow(window_name)
61
+ del self.windows[window_name]
62
+
63
+ def close_all(self):
64
+ """Closes all OpenCV windows."""
65
+ with self.lock:
66
+ for window in list(self.windows.keys()):
67
+ cv2.destroyWindow(window)
68
+ self.windows.clear()
69
+ cv2.waitKey(1)
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,197 @@
1
+ import argparse
2
+ import signal
3
+ import sys
4
+ import traceback
5
+ import logging
6
+
7
+ from .core_service import CoreService
8
+ from . import __version__
9
+
10
+
11
+ def signal_handler(signum, frame):
12
+ """Handle system signals for graceful shutdown"""
13
+ logging.info(f"Received signal {signum}, shutting down...")
14
+ sys.exit(0)
15
+
16
+
17
+ def main():
18
+ """Main CLI entry point."""
19
+ parser = argparse.ArgumentParser(
20
+ description="Nedo Vision Core Library CLI",
21
+ formatter_class=argparse.RawDescriptionHelpFormatter,
22
+ epilog="""
23
+ Examples:
24
+ # Start core service with default settings
25
+ nedo-core run
26
+
27
+ # Start with custom drawing assets path
28
+ nedo-core run --drawing-assets /path/to/assets
29
+
30
+ # Start with debug logging
31
+ nedo-core run --log-level DEBUG
32
+
33
+ # Start with custom storage path and RTMP server
34
+ nedo-core run --storage-path /path/to/storage --rtmp-server rtmp://server.com:1935/live
35
+
36
+ # Start with all custom parameters
37
+ nedo-core run --drawing-assets /path/to/assets --log-level DEBUG --storage-path /data --rtmp-server rtmp://server.com:1935/live
38
+
39
+ # Run system diagnostics
40
+ nedo-core doctor
41
+
42
+ Detection Callbacks:
43
+ The core service supports detection callbacks for extensible event handling.
44
+ See example_callbacks.py for usage examples.
45
+ """
46
+ )
47
+
48
+ # Add subcommands
49
+ subparsers = parser.add_subparsers(dest='command', help='Available commands')
50
+
51
+ # Run command
52
+ run_parser = subparsers.add_parser(
53
+ 'run',
54
+ help='Start the core service',
55
+ description='Start the Nedo Vision Core Service'
56
+ )
57
+
58
+ # Doctor command
59
+ doctor_parser = subparsers.add_parser(
60
+ 'doctor',
61
+ help='Run system diagnostics',
62
+ description='Run diagnostic checks for system requirements and dependencies'
63
+ )
64
+
65
+ run_parser.add_argument(
66
+ "--drawing-assets",
67
+ default=None,
68
+ help="Path to drawing assets directory (optional, uses bundled assets by default)"
69
+ )
70
+
71
+ run_parser.add_argument(
72
+ "--log-level",
73
+ choices=["DEBUG", "INFO", "WARNING", "ERROR"],
74
+ default="INFO",
75
+ help="Logging level (default: INFO)"
76
+ )
77
+
78
+ run_parser.add_argument(
79
+ "--storage-path",
80
+ default="data",
81
+ help="Storage path for databases and files (default: data)"
82
+ )
83
+
84
+ run_parser.add_argument(
85
+ "--rtmp-server",
86
+ default="rtmp://localhost:1935/live",
87
+ help="RTMP server URL for video streaming (default: rtmp://localhost:1935/live)"
88
+ )
89
+
90
+ # Add legacy arguments for backward compatibility (when no subcommand is used)
91
+ parser.add_argument(
92
+ "--drawing-assets",
93
+ help="(Legacy) Path to drawing assets directory (optional, uses bundled assets by default)"
94
+ )
95
+
96
+ parser.add_argument(
97
+ "--log-level",
98
+ choices=["DEBUG", "INFO", "WARNING", "ERROR"],
99
+ default="INFO",
100
+ help="(Legacy) Logging level (default: INFO)"
101
+ )
102
+
103
+ parser.add_argument(
104
+ "--storage-path",
105
+ default="data",
106
+ help="(Legacy) Storage path for databases and files (default: data)"
107
+ )
108
+
109
+ parser.add_argument(
110
+ "--rtmp-server",
111
+ default="rtmp://localhost:1935/live",
112
+ help="(Legacy) RTMP server URL for video streaming (default: rtmp://localhost:1935/live)"
113
+ )
114
+
115
+ parser.add_argument(
116
+ "--version",
117
+ action="version",
118
+ version=f"nedo-vision-core {__version__}"
119
+ )
120
+
121
+ args = parser.parse_args()
122
+
123
+ # Handle subcommands
124
+ if args.command == 'run':
125
+ run_core_service(args)
126
+ elif args.command == 'doctor':
127
+ run_doctor()
128
+ elif hasattr(args, 'drawing_assets') and args.drawing_assets is not None: # Legacy mode - if any arguments are provided without subcommand
129
+ print("⚠️ Warning: Using legacy command format. Consider using 'nedo-core run --drawing-assets ...' instead.")
130
+ run_core_service(args)
131
+ else:
132
+ # If no subcommand provided, show help
133
+ parser.print_help()
134
+ sys.exit(1)
135
+
136
+
137
+ def run_doctor():
138
+ """Run system diagnostics."""
139
+ try:
140
+ from .doctor import run_diagnostics
141
+ success = run_diagnostics()
142
+ sys.exit(0 if success else 1)
143
+ except ImportError:
144
+ print("❌ Doctor module not available")
145
+ sys.exit(1)
146
+ except Exception as e:
147
+ print(f"❌ Doctor failed: {e}")
148
+ sys.exit(1)
149
+
150
+
151
+ def run_core_service(args):
152
+ """Run the core service with the provided arguments."""
153
+
154
+ def run_core_service(args):
155
+ """Run the core service with the provided arguments."""
156
+ # Set up signal handlers for graceful shutdown
157
+ signal.signal(signal.SIGINT, signal_handler)
158
+ signal.signal(signal.SIGTERM, signal_handler)
159
+
160
+ logger = logging.getLogger(__name__)
161
+
162
+ try:
163
+ # Create and start the core service
164
+ service = CoreService(
165
+ drawing_assets_path=args.drawing_assets,
166
+ log_level=args.log_level,
167
+ storage_path=args.storage_path,
168
+ rtmp_server=args.rtmp_server
169
+ )
170
+
171
+ logger.info("🚀 Starting Nedo Vision Core...")
172
+ if args.drawing_assets:
173
+ logger.info(f"🎨 Drawing Assets: {args.drawing_assets}")
174
+ else:
175
+ logger.info("🎨 Drawing Assets: Using bundled assets")
176
+ logger.info(f"📝 Log Level: {args.log_level}")
177
+ logger.info(f"💾 Storage Path: {args.storage_path}")
178
+ logger.info(f"📡 RTMP Server: {args.rtmp_server}")
179
+ logger.info("Press Ctrl+C to stop the service")
180
+
181
+ # Start the service
182
+ success = service.run()
183
+
184
+ if success:
185
+ logger.info("✅ Core service completed successfully")
186
+ else:
187
+ logger.error("❌ Core service failed")
188
+ sys.exit(1)
189
+
190
+ except Exception as e:
191
+ logger.error(f"❌ Error: {e}")
192
+ traceback.print_exc()
193
+ sys.exit(1)
194
+
195
+
196
+ if __name__ == "__main__":
197
+ main()