nedo-vision-worker 1.2.6__tar.gz → 1.2.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/PKG-INFO +4 -3
  2. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/__init__.py +1 -1
  3. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/database/DatabaseManager.py +17 -1
  4. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/initializer/AppInitializer.py +60 -0
  5. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/worker_source_pipeline.py +2 -1
  6. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/repositories/PPEDetectionRepository.py +21 -2
  7. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/repositories/RestrictedAreaRepository.py +21 -2
  8. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/DirectDeviceToRTMPStreamer.py +12 -6
  9. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/FileToRTMPServer.py +7 -3
  10. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/GrpcClientBase.py +23 -34
  11. nedo_vision_worker-1.2.9/nedo_vision_worker/services/GrpcConnection.py +147 -0
  12. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/PPEDetectionClient.py +27 -8
  13. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/RTSPtoRTMPStreamer.py +10 -8
  14. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/RestrictedAreaClient.py +24 -3
  15. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/SystemUsageClient.py +2 -1
  16. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/WorkerSourcePipelineClient.py +25 -7
  17. nedo_vision_worker-1.2.9/nedo_vision_worker/util/EncoderSelector.py +109 -0
  18. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/util/SystemMonitor.py +3 -0
  19. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/DataSyncWorker.py +2 -2
  20. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/DatasetFrameWorker.py +1 -1
  21. nedo_vision_worker-1.2.9/nedo_vision_worker/worker/PipelinePreviewWorker.py +160 -0
  22. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/SystemUsageManager.py +22 -3
  23. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/WorkerManager.py +4 -0
  24. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker_service.py +10 -0
  25. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker.egg-info/PKG-INFO +4 -3
  26. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker.egg-info/SOURCES.txt +3 -0
  27. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker.egg-info/requires.txt +8 -6
  28. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/pyproject.toml +6 -3
  29. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/README.md +0 -0
  30. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/cli.py +0 -0
  31. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/config/ConfigurationManager.py +0 -0
  32. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/config/__init__.py +0 -0
  33. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/database/__init__.py +0 -0
  34. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/doctor.py +0 -0
  35. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/initializer/__init__.py +0 -0
  36. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/__init__.py +0 -0
  37. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/ai_model.py +0 -0
  38. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/auth.py +0 -0
  39. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/config.py +0 -0
  40. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/dataset_source.py +0 -0
  41. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/logs.py +0 -0
  42. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/ppe_detection.py +0 -0
  43. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/ppe_detection_label.py +0 -0
  44. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/restricted_area_violation.py +0 -0
  45. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/user.py +0 -0
  46. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/worker_source.py +0 -0
  47. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/worker_source_pipeline_config.py +0 -0
  48. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/worker_source_pipeline_debug.py +0 -0
  49. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/models/worker_source_pipeline_detection.py +0 -0
  50. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/AIModelService_pb2.py +0 -0
  51. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/AIModelService_pb2_grpc.py +0 -0
  52. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/DatasetSourceService_pb2.py +0 -0
  53. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/DatasetSourceService_pb2_grpc.py +0 -0
  54. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/HumanDetectionService_pb2.py +0 -0
  55. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/HumanDetectionService_pb2_grpc.py +0 -0
  56. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/PPEDetectionService_pb2.py +0 -0
  57. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/PPEDetectionService_pb2_grpc.py +0 -0
  58. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/VisionWorkerService_pb2.py +0 -0
  59. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/VisionWorkerService_pb2_grpc.py +0 -0
  60. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/WorkerSourcePipelineService_pb2.py +0 -0
  61. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/WorkerSourcePipelineService_pb2_grpc.py +0 -0
  62. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/WorkerSourceService_pb2.py +0 -0
  63. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/WorkerSourceService_pb2_grpc.py +0 -0
  64. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/protos/__init__.py +0 -0
  65. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/repositories/AIModelRepository.py +0 -0
  66. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/repositories/DatasetSourceRepository.py +0 -0
  67. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/repositories/WorkerSourcePipelineDebugRepository.py +0 -0
  68. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/repositories/WorkerSourcePipelineDetectionRepository.py +0 -0
  69. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/repositories/WorkerSourcePipelineRepository.py +0 -0
  70. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/repositories/WorkerSourceRepository.py +0 -0
  71. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/repositories/__init__.py +0 -0
  72. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/AIModelClient.py +0 -0
  73. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/ConnectionInfoClient.py +0 -0
  74. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/DatasetSourceClient.py +0 -0
  75. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/GrpcClientManager.py +0 -0
  76. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/ImageUploadClient.py +0 -0
  77. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/SharedDirectDeviceClient.py +0 -0
  78. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/SharedVideoStreamServer.py +0 -0
  79. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/SystemWideDeviceCoordinator.py +0 -0
  80. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/VideoSharingDaemon.py +0 -0
  81. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/VideoStreamClient.py +0 -0
  82. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/WorkerSourceClient.py +0 -0
  83. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/WorkerSourceUpdater.py +0 -0
  84. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/WorkerStatusClient.py +0 -0
  85. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/services/__init__.py +0 -0
  86. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/util/FFmpegUtil.py +0 -0
  87. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/util/HardwareID.py +0 -0
  88. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/util/ImageUploader.py +0 -0
  89. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/util/Networking.py +0 -0
  90. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/util/PlatformDetector.py +0 -0
  91. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/util/VideoProbeUtil.py +0 -0
  92. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/util/__init__.py +0 -0
  93. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/CoreActionWorker.py +0 -0
  94. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/DataSenderWorker.py +0 -0
  95. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/DatasetFrameSender.py +0 -0
  96. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/PPEDetectionManager.py +0 -0
  97. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/PipelineActionWorker.py +0 -0
  98. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/PipelineImageWorker.py +0 -0
  99. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/RabbitMQListener.py +0 -0
  100. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/RestrictedAreaManager.py +0 -0
  101. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/VideoStreamWorker.py +0 -0
  102. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker/worker/__init__.py +0 -0
  103. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker.egg-info/dependency_links.txt +0 -0
  104. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker.egg-info/entry_points.txt +0 -0
  105. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/nedo_vision_worker.egg-info/top_level.txt +0 -0
  106. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/setup.cfg +0 -0
  107. {nedo_vision_worker-1.2.6 → nedo_vision_worker-1.2.9}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nedo-vision-worker
3
- Version: 1.2.6
3
+ Version: 1.2.9
4
4
  Summary: Nedo Vision Worker Service Library for AI Vision Processing
5
5
  Author-email: Willy Achmat Fauzi <willy.achmat@gmail.com>
6
6
  Maintainer-email: Willy Achmat Fauzi <willy.achmat@gmail.com>
@@ -40,9 +40,10 @@ Requires-Dist: protobuf>=3.20.0
40
40
  Requires-Dist: psutil>=5.9.0
41
41
  Requires-Dist: requests>=2.28.0
42
42
  Requires-Dist: SQLAlchemy>=1.4.0
43
- Requires-Dist: opencv-python>=4.6.0; platform_machine not in "aarch64 armv7l"
44
- Requires-Dist: opencv-python-headless>=4.6.0; platform_machine in "aarch64 armv7l"
45
43
  Requires-Dist: pynvml>=11.4.1; platform_system != "Darwin" or platform_machine != "arm64"
44
+ Provides-Extra: opencv
45
+ Requires-Dist: opencv-python>=4.6.0; platform_machine not in "aarch64 armv7l" and extra == "opencv"
46
+ Requires-Dist: opencv-python-headless>=4.6.0; platform_machine in "aarch64 armv7l" and extra == "opencv"
46
47
  Provides-Extra: dev
47
48
  Requires-Dist: pytest>=7.0.0; extra == "dev"
48
49
  Requires-Dist: black>=22.0.0; extra == "dev"
@@ -6,5 +6,5 @@ A library for running worker agents in the Nedo Vision platform.
6
6
 
7
7
  from .worker_service import WorkerService
8
8
 
9
- __version__ = "1.2.6"
9
+ __version__ = "1.2.9"
10
10
  __all__ = ["WorkerService"]
@@ -104,7 +104,23 @@ class DatabaseManager:
104
104
  # Initialize engines and session factories for each database
105
105
  for name, path in DB_PATHS.items():
106
106
  path.parent.mkdir(parents=True, exist_ok=True) # Ensure directory exists
107
- engine = create_engine(f"sqlite:///{path.as_posix()}")
107
+
108
+ # Configure connection pool for multi-threaded usage
109
+ # pool_size: Max connections to keep open
110
+ # max_overflow: Additional connections that can be created temporarily
111
+ # pool_pre_ping: Test connections before using (prevents stale connections)
112
+ # pool_recycle: Recycle connections after N seconds (prevents long-lived stale connections)
113
+ engine = create_engine(
114
+ f"sqlite:///{path.as_posix()}",
115
+ pool_size=20, # Base pool size for persistent connections
116
+ max_overflow=30, # Allow up to 30 additional temporary connections
117
+ pool_pre_ping=True, # Verify connection health before use
118
+ pool_recycle=3600, # Recycle connections after 1 hour
119
+ connect_args={
120
+ "check_same_thread": False, # Required for SQLite with multiple threads
121
+ "timeout": 30.0 # Connection timeout
122
+ }
123
+ )
108
124
  ENGINES[name] = engine
109
125
  SESSION_FACTORIES[name] = scoped_session(sessionmaker(bind=engine)) # Use scoped sessions
110
126
  DatabaseManager.synchronize(name)
@@ -76,3 +76,63 @@ class AppInitializer:
76
76
  logging.error(f"Grpc Error: {ge}")
77
77
  except Exception as e:
78
78
  logging.error(f"Unexpected error during initialization: {e}")
79
+
80
+ @staticmethod
81
+ def update_connection_info(server_host: str, server_port: int, token: str):
82
+ """
83
+ Fetch and update connection information (RabbitMQ credentials) from the server.
84
+ This should be called on startup to ensure credentials are up-to-date.
85
+
86
+ Args:
87
+ server_host: The server hostname or IP address
88
+ server_port: The gRPC server port
89
+ token: Authentication token for the worker
90
+
91
+ Returns:
92
+ bool: True if update was successful, False otherwise
93
+ """
94
+ try:
95
+ # Validate server host
96
+ AppInitializer.validate_server_host(server_host)
97
+
98
+ # Get connection info using the ConnectionInfoClient
99
+ connection_client = ConnectionInfoClient(server_host, server_port, token)
100
+ connection_result = connection_client.get_connection_info()
101
+
102
+ if not connection_result["success"]:
103
+ logging.error(f"Failed to fetch connection info: {connection_result['message']}")
104
+ return False
105
+
106
+ # Check if any RabbitMQ credentials have changed
107
+ current_config = ConfigurationManager.get_all_configs()
108
+ config_updated = False
109
+
110
+ rabbitmq_fields = {
111
+ 'rabbitmq_host': connection_result['rabbitmq_host'],
112
+ 'rabbitmq_port': str(connection_result['rabbitmq_port']),
113
+ 'rabbitmq_username': connection_result['rabbitmq_username'],
114
+ 'rabbitmq_password': connection_result['rabbitmq_password']
115
+ }
116
+
117
+ for field, new_value in rabbitmq_fields.items():
118
+ if current_config.get(field) != new_value:
119
+ ConfigurationManager.set_config(field, new_value)
120
+ config_updated = True
121
+ logging.info(f"✅ [APP] Updated {field}")
122
+
123
+ if config_updated:
124
+ logging.info("✅ [APP] RabbitMQ connection info updated successfully")
125
+ else:
126
+ logging.info("✅ [APP] RabbitMQ connection info is up-to-date")
127
+
128
+ return True
129
+
130
+ except ValueError as ve:
131
+ logging.error(f"Validation error: {ve}")
132
+ return False
133
+ except grpc.RpcError as ge:
134
+ logging.error(f"gRPC Error: {ge}")
135
+ return False
136
+ except Exception as e:
137
+ logging.error(f"Unexpected error updating connection info: {e}")
138
+ return False
@@ -1,4 +1,4 @@
1
- from sqlalchemy import Column, String
1
+ from sqlalchemy import Column, String, DateTime
2
2
  from sqlalchemy.orm import relationship
3
3
  from ..database.DatabaseManager import Base
4
4
 
@@ -13,6 +13,7 @@ class WorkerSourcePipelineEntity(Base):
13
13
  ai_model_id = Column(String, nullable=True)
14
14
  pipeline_status_code = Column(String, nullable=False)
15
15
  location_name = Column(String, nullable=True)
16
+ last_preview_request_at = Column(DateTime, nullable=True)
16
17
 
17
18
  worker_source_pipeline_configs = relationship(
18
19
  "WorkerSourcePipelineConfigEntity",
@@ -77,6 +77,10 @@ class PPEDetectionRepository:
77
77
  Args:
78
78
  detection_data (list): List of dictionaries containing the detection data.
79
79
  """
80
+ if not detection_data:
81
+ logging.info("No detection data provided for deletion.")
82
+ return
83
+
80
84
  try:
81
85
  # Extract person_id from detection data to delete the corresponding records
82
86
  person_ids_to_delete = [data['person_id'] for data in detection_data]
@@ -97,10 +101,25 @@ class PPEDetectionRepository:
97
101
  if not os.path.isabs(image_path):
98
102
  image_path = str(get_storage_path("files") / Path(image_path).relative_to("data/files"))
99
103
  if os.path.exists(image_path):
100
- os.remove(image_path)
101
- logging.info(f"Deleted image file: {image_path}")
104
+ try:
105
+ os.remove(image_path)
106
+ logging.info(f"Deleted image file: {image_path}")
107
+ except OSError as e:
108
+ logging.warning(f"Failed to delete image file {image_path}: {e}")
102
109
  else:
103
110
  logging.warning(f"Image file not found for detection {detection.id}: {image_path}")
111
+
112
+ # Delete the image tile file if it exists
113
+ image_tile_path = detection.image_tile_path
114
+ if image_tile_path:
115
+ if not os.path.isabs(image_tile_path):
116
+ image_tile_path = str(get_storage_path("files") / Path(image_tile_path).relative_to("data/files"))
117
+ if os.path.exists(image_tile_path):
118
+ try:
119
+ os.remove(image_tile_path)
120
+ logging.info(f"Deleted image tile file: {image_tile_path}")
121
+ except OSError as e:
122
+ logging.warning(f"Failed to delete image tile file {image_tile_path}: {e}")
104
123
 
105
124
  # Delete the detection record
106
125
  self.session.delete(detection)
@@ -61,6 +61,10 @@ class RestrictedAreaRepository:
61
61
  Args:
62
62
  violation_data (list): List of dictionaries containing the violation data.
63
63
  """
64
+ if not violation_data:
65
+ logging.info("No violation data provided for deletion.")
66
+ return
67
+
64
68
  try:
65
69
  person_ids_to_delete = [data['person_id'] for data in violation_data]
66
70
 
@@ -75,11 +79,26 @@ class RestrictedAreaRepository:
75
79
  if not os.path.isabs(image_path):
76
80
  image_path = str(get_storage_path("restricted_violations") / Path(image_path).relative_to("data/restricted_violations"))
77
81
  if os.path.exists(image_path):
78
- os.remove(image_path)
79
- logging.info(f"Deleted image file: {image_path}")
82
+ try:
83
+ os.remove(image_path)
84
+ logging.info(f"Deleted image file: {image_path}")
85
+ except OSError as e:
86
+ logging.warning(f"Failed to delete image file {image_path}: {e}")
80
87
  else:
81
88
  logging.warning(f"Image file not found for violation {violation.id}: {image_path}")
82
89
 
90
+ # Delete the image tile file if it exists
91
+ image_tile_path = violation.image_tile_path
92
+ if image_tile_path:
93
+ if not os.path.isabs(image_tile_path):
94
+ image_tile_path = str(get_storage_path("restricted_violations") / Path(image_tile_path).relative_to("data/restricted_violations"))
95
+ if os.path.exists(image_tile_path):
96
+ try:
97
+ os.remove(image_tile_path)
98
+ logging.info(f"Deleted image tile file: {image_tile_path}")
99
+ except OSError as e:
100
+ logging.warning(f"Failed to delete image tile file {image_tile_path}: {e}")
101
+
83
102
  self.session.delete(violation)
84
103
 
85
104
  self.session.commit()
@@ -8,6 +8,7 @@ import cv2
8
8
  import os
9
9
  from .VideoSharingDaemon import VideoSharingClient
10
10
  from ..database.DatabaseManager import get_storage_path
11
+ from ..util.EncoderSelector import EncoderSelector
11
12
 
12
13
 
13
14
  class DirectDeviceToRTMPStreamer:
@@ -68,6 +69,10 @@ class DirectDeviceToRTMPStreamer:
68
69
 
69
70
  def _start_ffmpeg_stream(self):
70
71
  """Starts an FFmpeg process to stream frames to the RTMP server silently."""
72
+ # Get optimal encoder for hardware
73
+ encoder_args, encoder_name = EncoderSelector.get_encoder_args()
74
+ logging.info(f"🎬 [APP] Using encoder: {encoder_name}")
75
+
71
76
  ffmpeg_command = [
72
77
  "ffmpeg",
73
78
  "-y",
@@ -79,16 +84,17 @@ class DirectDeviceToRTMPStreamer:
79
84
  "-video_size", f"{self.width}x{self.height}",
80
85
  "-framerate", str(self.fps),
81
86
  "-i", "-",
82
- "-c:v", "libx264",
83
- "-preset", "ultrafast",
84
- "-tune", "zerolatency",
87
+
88
+ # Video encoding with optimal encoder
89
+ *encoder_args,
85
90
  "-b:v", self.bitrate,
86
- # Disable Audio (Avoid unnecessary encoding overhead)
87
- "-an",
88
91
  "-maxrate", "2500k",
89
92
  "-bufsize", "5000k",
93
+
94
+ # Disable Audio
95
+ "-an",
96
+
90
97
  "-f", "flv",
91
- # Remove duration limit - let application control duration
92
98
  self.rtmp_url,
93
99
  ]
94
100
 
@@ -1,6 +1,7 @@
1
1
  import subprocess
2
2
  import logging
3
3
  import os
4
+ from ..util.EncoderSelector import EncoderSelector
4
5
 
5
6
  class FileToRTMPStreamer:
6
7
  def __init__(self, video_path, rtmp_url, stream_key, fps=30, resolution="1280x720", loop=False):
@@ -31,6 +32,10 @@ class FileToRTMPStreamer:
31
32
 
32
33
  logging.info(f"📼 [APP] Starting file stream: {self.video_path} → {self.rtmp_url}")
33
34
 
35
+ # Get optimal encoder for hardware
36
+ encoder_args, encoder_name = EncoderSelector.get_encoder_args()
37
+ logging.info(f"🎬 [APP] Using encoder: {encoder_name}")
38
+
34
39
  # FFmpeg command
35
40
  ffmpeg_command = [
36
41
  "ffmpeg",
@@ -38,9 +43,8 @@ class FileToRTMPStreamer:
38
43
  "-stream_loop", "-1" if self.loop else "0", # Loop if needed
39
44
  "-i", self.video_path,
40
45
 
41
- "-c:v", "libx264",
42
- "-preset", "ultrafast",
43
- "-tune", "zerolatency",
46
+ # Video encoding with optimal encoder
47
+ *encoder_args,
44
48
  "-r", str(self.fps),
45
49
  "-b:v", "1500k",
46
50
  "-maxrate", "2000k",
@@ -3,6 +3,7 @@ import logging
3
3
  import time
4
4
  from grpc import StatusCode
5
5
  from typing import Callable, Optional, Any, Dict
6
+ from .GrpcConnection import GrpcConnection
6
7
 
7
8
  logger = logging.getLogger(__name__)
8
9
 
@@ -42,42 +43,21 @@ class GrpcClientBase:
42
43
  def __init__(self, server_host: str, server_port: int = 50051, max_retries: int = 3):
43
44
  self.server_address = f"{server_host}:{server_port}"
44
45
  self.channel: Optional[grpc.Channel] = None
45
- self.stub: Optional[Any] = None
46
46
  self.connected = False
47
47
  self.max_retries = max_retries
48
48
 
49
+ self.connection = GrpcConnection(server_host, server_port)
50
+
49
51
  def connect(self, stub_class, retry_interval: int = 2) -> bool:
50
- attempts = 0
51
- while attempts < self.max_retries and not self.connected:
52
- try:
53
- if self.channel:
54
- self._close_channel()
55
-
56
- self.channel = grpc.insecure_channel(self.server_address)
57
-
58
- future = grpc.channel_ready_future(self.channel)
59
- future.result(timeout=30)
60
-
61
- self.stub = stub_class(self.channel)
62
- self.connected = True
63
- logger.info(f"🚀 Connected to gRPC server at {self.server_address}")
64
- return True
65
-
66
- except (grpc.RpcError, grpc.FutureTimeoutError, Exception) as e:
67
- attempts += 1
68
- self.connected = False
69
- error_msg = str(e)
70
-
71
- logger.error(f"⚠️ Connection failed ({attempts}/{self.max_retries}): {error_msg}", exc_info=True)
72
-
73
- if attempts < self.max_retries:
74
- sleep_time = retry_interval * (2 ** (attempts - 1))
75
- logger.info(f"⏳ Retrying in {sleep_time}s...")
76
- time.sleep(sleep_time)
77
- else:
78
- logger.critical("❌ Max retries reached. Connection failed.")
79
-
80
- return False
52
+ conn = self.connection.get_connection()
53
+ if conn is None:
54
+ return False
55
+ requested_stub = stub_class(conn)
56
+
57
+ self.stub = requested_stub
58
+ self.connected = True
59
+
60
+ return True
81
61
 
82
62
  def _close_channel(self) -> None:
83
63
  try:
@@ -89,6 +69,7 @@ class GrpcClientBase:
89
69
  self.channel = None
90
70
  self.stub = None
91
71
 
72
+ # MARK:
92
73
  def close(self) -> None:
93
74
  self._close_channel()
94
75
  self.connected = False
@@ -104,6 +85,11 @@ class GrpcClientBase:
104
85
  except grpc.RpcError as e:
105
86
  return self._handle_grpc_error(e, rpc_call, *args, **kwargs)
106
87
  except Exception as e:
88
+ print(e)
89
+ print(str(e) == "Cannot invoke RPC on closed channel!")
90
+ if str(e) == "Cannot invoke RPC on closed channel!":
91
+ self.connect(type(self.stub))
92
+
107
93
  logger.error(f"💥 Unexpected RPC error: {e}")
108
94
  return None
109
95
 
@@ -127,8 +113,11 @@ class GrpcClientBase:
127
113
 
128
114
  return None
129
115
 
116
+ # MARK:
117
+ # Should request for reconnection two times. Notify grpc connection to do reconnect
130
118
  def _handle_unavailable(self, rpc_call: Callable, *args, **kwargs) -> Optional[Any]:
131
- self.connected = False
119
+ # self.connected = False
120
+ self.connection.try_reconnect()
132
121
 
133
122
  if self.stub:
134
123
  stub_class = type(self.stub)
@@ -154,7 +143,7 @@ class GrpcClientBase:
154
143
  return error_message.split("debug_error_string")[0].strip()
155
144
 
156
145
  def is_connected(self) -> bool:
157
- return self.connected and self.channel and self.stub
146
+ return self.connected and self.connection.get_connection() is not None and self.stub is not None
158
147
 
159
148
  def get_connection_info(self) -> Dict[str, Any]:
160
149
  return {
@@ -0,0 +1,147 @@
1
+ import grpc
2
+ import logging
3
+ import time
4
+ import threading
5
+ from grpc import StatusCode
6
+ from typing import Optional, Any, Dict
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class GrpcConnection:
12
+ """
13
+ Grpc connection management. Responsible for initiating connection with gRPC server that
14
+ will be used across gRPC clients in the project.
15
+ Expected to be initiated as a singleton.
16
+ """
17
+
18
+ _instance = None
19
+ _init_done = False
20
+ _lock = threading.Lock()
21
+ _reconnectLock = threading.Lock()
22
+ _reconnecting = False
23
+
24
+ def __new__(cls, *args, **kwargs):
25
+ if cls._instance is None:
26
+ with cls._lock:
27
+ if cls._instance is None:
28
+ cls._instance = super().__new__(cls)
29
+ return cls._instance
30
+
31
+ def __init__(self, server_host: str, server_port: int = 50051, max_retries: int = 3):
32
+ if self.__class__._init_done:
33
+ return # prevent re-initialization
34
+ self.__class__._init_done = True
35
+
36
+ self.server_address = f"{server_host}:{server_port}"
37
+ self.channel: Optional[grpc.Channel] = None
38
+ self.connected = False
39
+ self.max_retries = max_retries
40
+ self.connect()
41
+
42
+ def connect(self, retry_interval: int = 2) -> bool:
43
+ attempts = 0
44
+ while attempts < self.max_retries and not self.connected:
45
+ try:
46
+ if self.channel:
47
+ self._close_channel()
48
+
49
+ self.channel = grpc.insecure_channel(self.server_address)
50
+
51
+ future = grpc.channel_ready_future(self.channel)
52
+ future.result(timeout=30)
53
+
54
+ self.connected = True
55
+ logger.info(f"🚀 Connected to gRPC server at {self.server_address}")
56
+ return True
57
+
58
+ except (grpc.RpcError, grpc.FutureTimeoutError, Exception) as e:
59
+ attempts += 1
60
+ self.connected = False
61
+ error_msg = str(e)
62
+
63
+ logger.error(f"⚠️ Connection failed ({attempts}/{self.max_retries}): {error_msg}")
64
+
65
+ if attempts < self.max_retries:
66
+ sleep_time = retry_interval * (2 ** (attempts - 1))
67
+ logger.info(f"⏳ Retrying in {sleep_time}s...")
68
+ time.sleep(sleep_time)
69
+ else:
70
+ logger.critical("❌ Max retries reached. Connection failed.")
71
+
72
+ return False
73
+
74
+ def get_connection(self):
75
+ if self._reconnecting or not self.connected:
76
+ return None
77
+
78
+ return self.channel
79
+
80
+ def _reconnect(self):
81
+ logger.info(f"⏳ Reconnecting...")
82
+ attempts = 0
83
+
84
+ while not self.connected:
85
+ try:
86
+ if self.channel:
87
+ self._close_channel()
88
+
89
+ self.channel = grpc.insecure_channel(self.server_address)
90
+
91
+ future = grpc.channel_ready_future(self.channel)
92
+ future.result(timeout=30)
93
+
94
+ self.connected = True
95
+ self._reconnecting = False
96
+ logger.info(f"🚀 Connected to gRPC server at {self.server_address}")
97
+ self._reconnectLock.release_lock()
98
+ except (grpc.RpcError, grpc.FutureTimeoutError, Exception) as e:
99
+ attempts += 1
100
+ self.connected = False
101
+ error_msg = str(e)
102
+
103
+ logger.error(f"⚠️ Connection failed ({attempts}/{self.max_retries}): {error_msg}")
104
+
105
+ sleep_time = 2 * (2 ** (attempts - 1))
106
+ logger.info(f"⏳ Retrying in {sleep_time}s...")
107
+ time.sleep(sleep_time)
108
+
109
+ def try_reconnect(self):
110
+ if self._reconnecting:
111
+ return
112
+
113
+ if self._reconnectLock.acquire_lock(blocking=False):
114
+ self._reconnecting = True
115
+ self.connected = False
116
+ self._reconnect()
117
+
118
+ def _close_channel(self) -> None:
119
+ try:
120
+ if self.channel:
121
+ self.channel.close()
122
+ logger.info("🔌 gRPC connection closed")
123
+ except Exception as e:
124
+ logger.warning(f"⚠️ Error closing channel: {e}")
125
+ finally:
126
+ self.channel = None
127
+ self.connected = False
128
+
129
+ def close(self) -> None:
130
+ if self.channel:
131
+ self.channel.close()
132
+ self.connected = False
133
+ logger.info("🔌 gRPC connection closed")
134
+
135
+ def is_connected(self) -> bool:
136
+ return self.connected and self.channel is not None
137
+
138
+ def get_connection_info(self) -> Dict[str, Any]:
139
+ return {
140
+ "server_address": self.server_address,
141
+ "connected": self.connected,
142
+ "max_retries": self.max_retries,
143
+ "has_channel": self.channel is not None,
144
+ }
145
+
146
+ def __enter__(self):
147
+ return self
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ import os
2
3
  from .GrpcClientBase import GrpcClientBase
3
4
  from ..protos.PPEDetectionService_pb2_grpc import PPEDetectionGRPCServiceStub
4
5
  from ..protos.PPEDetectionService_pb2 import UpsertPPEDetectionBatchRequest, UpsertPPEDetectionRequest, PPEDetectionLabelRequest
@@ -56,11 +57,23 @@ class PPEDetectionClient(GrpcClientBase):
56
57
  return {"success": False, "message": "gRPC connection is not established."}
57
58
 
58
59
  try:
59
- # Prepare the list of UpsertPPEDetectionRequest messages
60
60
  ppe_detection_requests = []
61
+ valid_records = []
62
+ invalid_records = []
63
+
61
64
  for data in detection_data:
62
- image_binary = self.read_image_as_binary(data['image'])
63
- image_tile_binary = self.read_image_as_binary(data['image_tile'])
65
+ if not os.path.exists(data['image']) or not os.path.exists(data['image_tile']):
66
+ logger.warning(f"⚠️ Missing image files for person_id {data.get('person_id')}")
67
+ invalid_records.append(data)
68
+ continue
69
+
70
+ try:
71
+ image_binary = self.read_image_as_binary(data['image'])
72
+ image_tile_binary = self.read_image_as_binary(data['image_tile'])
73
+ except Exception as e:
74
+ logger.error(f"❌ Error reading images for person_id {data.get('person_id')}: {e}")
75
+ invalid_records.append(data)
76
+ continue
64
77
 
65
78
  ppe_detection_labels = [
66
79
  PPEDetectionLabelRequest(
@@ -73,11 +86,11 @@ class PPEDetectionClient(GrpcClientBase):
73
86
  )
74
87
  for label in data['ppe_detection_labels']
75
88
  ]
76
- source = data['worker_source_id']
89
+
77
90
  request = UpsertPPEDetectionRequest(
78
91
  person_id=data['person_id'],
79
92
  worker_id=worker_id,
80
- worker_source_id=source,
93
+ worker_source_id=data['worker_source_id'],
81
94
  image=image_binary,
82
95
  image_tile=image_tile_binary,
83
96
  worker_timestamp=data['worker_timestamp'],
@@ -85,18 +98,24 @@ class PPEDetectionClient(GrpcClientBase):
85
98
  token=token
86
99
  )
87
100
  ppe_detection_requests.append(request)
101
+ valid_records.append(data)
102
+
103
+ if invalid_records:
104
+ logger.info(f"🧹 Deleting {len(invalid_records)} invalid PPE detection records")
105
+ self.repository.delete_records_from_db(invalid_records)
106
+
107
+ if not ppe_detection_requests:
108
+ return {"success": True, "message": "No valid detections to send"}
88
109
 
89
- # Create the UpsertPPEDetectionBatchRequest
90
110
  batch_request = UpsertPPEDetectionBatchRequest(
91
111
  ppe_detection_requests=ppe_detection_requests,
92
112
  token=token
93
113
  )
94
114
 
95
- # Call the UpsertBatch RPC
96
115
  response = self.handle_rpc(self.stub.UpsertBatch, batch_request)
97
116
 
98
117
  if response and response.success:
99
- self.repository.delete_records_from_db(detection_data)
118
+ self.repository.delete_records_from_db(valid_records)
100
119
  return {"success": True, "message": response.message}
101
120
 
102
121
  return {"success": False, "message": response.message if response else "Unknown error"}
@@ -3,6 +3,7 @@ import logging
3
3
  import time
4
4
  import os
5
5
  from urllib.parse import urlparse
6
+ from ..util.EncoderSelector import EncoderSelector
6
7
 
7
8
  class RTSPtoRTMPStreamer:
8
9
  def __init__(self, rtsp_url, rtmp_url, stream_key, fps=30, resolution="1280x720", duration=120):
@@ -38,6 +39,10 @@ class RTSPtoRTMPStreamer:
38
39
 
39
40
  logging.info(f"📡 [APP] Starting RTSP to RTMP stream: {self.rtsp_url} → {self.rtmp_url} for {self.duration} seconds")
40
41
 
42
+ # Get optimal encoder for hardware
43
+ encoder_args, encoder_name = EncoderSelector.get_encoder_args()
44
+ logging.info(f"🎬 [APP] Using encoder: {encoder_name}")
45
+
41
46
  # FFmpeg command
42
47
  ffmpeg_command = [
43
48
  "ffmpeg",
@@ -47,17 +52,14 @@ class RTSPtoRTMPStreamer:
47
52
  "-strict", "experimental",
48
53
  "-i", self.rtsp_url,
49
54
 
50
- # Video Encoding (Fastest possible)
51
- "-c:v", "libx264",
52
- "-preset", "ultrafast", # 🚀 Reduce CPU usage
53
- "-tune", "zerolatency", # 🚀 Optimize for real-time streaming
54
- "-x264-params", "keyint=40:min-keyint=40", # 🚀 Keyframe optimization
55
- "-r", "25", # ⏳ Limit FPS to 20 (prevents excessive encoding load)
56
- "-b:v", "1500k", # ✅ Lower bitrate to improve performance
55
+ # Video encoding with optimal encoder
56
+ *encoder_args,
57
+ "-r", "25", # Limit FPS to 25
58
+ "-b:v", "1500k", # Bitrate
57
59
  "-maxrate", "2000k", # ✅ Set max bitrate
58
60
  "-bufsize", "4000k", # ✅ Reduce buffer latency
59
61
  "-g", "25", # ✅ Reduce GOP size for faster keyframes
60
- "-vf", "scale='min(1024,iw)':-2", # ✅ Resize width to max 800px
62
+ "-vf", "scale='min(1024,iw)':-2", # ✅ Resize width to max 1024px
61
63
 
62
64
  # ❌ Disable Audio (Avoid unnecessary encoding overhead)
63
65
  "-an",