matrice-analytics 0.1.31__py3-none-any.whl → 0.1.32__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 matrice-analytics might be problematic. Click here for more details.

@@ -3,6 +3,7 @@ import logging
3
3
  import time
4
4
  import threading
5
5
  import queue
6
+ import base64
6
7
  from typing import Dict, Optional, Set
7
8
  import numpy as np
8
9
  import cv2
@@ -169,7 +170,7 @@ class PeopleActivityLogging:
169
170
  return True
170
171
 
171
172
  async def _process_activity(self, activity_data: Dict):
172
- """Process activity data - handle all face detections and uploads"""
173
+ """Process activity data - handle all face detections with embedded image data"""
173
174
  detection_type = activity_data["detection_type"]
174
175
  current_frame = activity_data["current_frame"]
175
176
  bbox = activity_data["bbox"]
@@ -188,27 +189,57 @@ class PeopleActivityLogging:
188
189
  self.logger.debug(f"Skipping activity log for employee_id={employee_id} (within cooldown period)")
189
190
  return None
190
191
 
191
- # Store activity data
192
+ # Encode frame as base64 JPEG
193
+ image_data = None
194
+ if current_frame is not None:
195
+ try:
196
+ self.logger.debug(f"Encoding frame as base64 JPEG - employee_id={employee_id}")
197
+ _, buffer = cv2.imencode(".jpg", current_frame)
198
+ frame_bytes = buffer.tobytes()
199
+ image_data = base64.b64encode(frame_bytes).decode('utf-8')
200
+ self.logger.debug(f"Encoded image data - employee_id={employee_id}, size={len(frame_bytes)} bytes")
201
+ except Exception as e:
202
+ self.logger.error(f"Error encoding frame for employee_id={employee_id}: {e}", exc_info=True)
203
+
204
+ # Store activity data with embedded image
192
205
  self.logger.info(f"Processing activity log - type={detection_type}, employee_id={employee_id}, staff_id={staff_id}, location={location}")
193
- upload_url = await self.face_client.store_people_activity(
206
+ response = await self.face_client.store_people_activity(
194
207
  staff_id=staff_id,
195
208
  detection_type=detection_type,
196
209
  bbox=bbox,
197
210
  location=location,
198
211
  employee_id=employee_id,
199
212
  timestamp=timestamp,
213
+ image_data=image_data,
200
214
  )
201
215
 
202
- if upload_url:
203
- self.logger.info(f"Activity log stored successfully, upload URL received for employee_id={employee_id}")
204
- await self._upload_frame(current_frame, upload_url, employee_id)
216
+ if response and response.get("success", False):
217
+ self.logger.info(f"Activity log stored successfully for employee_id={employee_id}")
205
218
  else:
206
- self.logger.warning(f"Failed to store activity log for employee_id={employee_id} - no upload URL returned")
219
+ error_msg = response.get("error", "Unknown error") if response else "No response"
220
+ self.logger.warning(f"Failed to store activity log for employee_id={employee_id} - {error_msg}")
207
221
 
208
- return upload_url
222
+ return response
209
223
  except Exception as e:
210
224
  self.logger.error(f"Error processing activity log for employee_id={employee_id}: {e}", exc_info=True)
211
-
225
+
226
+ # async def _upload_frame_to_url(self, current_frame: np.ndarray, upload_url: str, employee_id: str):
227
+ # try:
228
+ # self.logger.debug(f"Encoding frame for upload - employee_id={employee_id}")
229
+ # _, buffer = cv2.imencode(".jpg", current_frame)
230
+ # frame_bytes = buffer.tobytes()
231
+
232
+ # self.logger.info(f"Uploading frame to storage - employee_id={employee_id}, size={len(frame_bytes)} bytes")
233
+ # upload_success = await self.face_client.upload_image_to_url(
234
+ # frame_bytes, upload_url
235
+ # )
236
+
237
+ # if upload_success:
238
+ # self.logger.info(f"Frame uploaded successfully for employee_id={employee_id}")
239
+ # else:
240
+ # self.logger.warning(f"Failed to upload frame for employee_id={employee_id}")
241
+ # except Exception as e:
242
+ # self.logger.error(f"Error uploading frame for employee_id={employee_id}: {e}", exc_info=True)
212
243
 
213
244
  async def _upload_frame(self, current_frame: np.ndarray, upload_url: str, employee_id: str):
214
245
  try:
@@ -320,6 +320,7 @@ class PostProcessor:
320
320
  f"Removing facial_recognition_server_id from {usecase} config"
321
321
  )
322
322
  config_params.pop("facial_recognition_server_id", None)
323
+ config_params.pop("deployment_id", None)
323
324
 
324
325
  if usecase not in license_plate_monitoring_usecases:
325
326
  if "lpr_server_id" in config_params:
@@ -642,7 +643,7 @@ class PostProcessor:
642
643
  config_str = json.dumps(cache_data, sort_keys=True, default=str)
643
644
  return hashlib.md5(config_str.encode()).hexdigest()[:16] # Shorter hash for readability
644
645
 
645
- def _get_use_case_instance(
646
+ async def _get_use_case_instance(
646
647
  self, config: BaseConfig, stream_key: Optional[str] = None
647
648
  ):
648
649
  """
@@ -669,8 +670,10 @@ class PostProcessor:
669
670
  raise ValueError(f"Use case '{config.category}/{config.usecase}' not found")
670
671
 
671
672
 
672
- if isinstance(use_case_class, FaceRecognitionEmbeddingUseCase):
673
+ if use_case_class == FaceRecognitionEmbeddingUseCase:
673
674
  use_case = use_case_class(config=config)
675
+ # Await async initialization for face recognition use case
676
+ await use_case.initialize(config)
674
677
  else:
675
678
  use_case = use_case_class()
676
679
  logger.info(f"Created use case instance for: {config.category}/{config.usecase}")
@@ -759,16 +762,20 @@ class PostProcessor:
759
762
 
760
763
  try:
761
764
  if config:
762
- parsed_config = self._parse_config(config)
763
- else:
764
- parsed_config = self.post_processing_config
765
+ try:
766
+ config = self._parse_config(config)
767
+ except Exception as e:
768
+ logger.error(f"Failed to parse config: {e}", exc_info=True)
769
+ raise ValueError(f"Failed to parse config: {e}")
770
+
771
+ parsed_config = config or self.post_processing_config
765
772
 
766
773
  if not parsed_config:
767
774
  raise ValueError("No valid configuration found")
768
775
 
769
776
 
770
- # Get cached use case instance
771
- use_case = self._get_use_case_instance(parsed_config, stream_key)
777
+ # Get cached use case instance (await since it's async now)
778
+ use_case = await self._get_use_case_instance(parsed_config, stream_key)
772
779
 
773
780
  # Create context if not provided
774
781
  if context is None:
@@ -86,7 +86,7 @@ class ColorDetectionConfig(BaseConfig):
86
86
  smoothing_window_size: int = 20
87
87
  smoothing_cooldown_frames: int = 5
88
88
  smoothing_confidence_range_factor: float = 0.5
89
- detector = True
89
+ enable_detector: bool = True
90
90
 
91
91
  #JBK_720_GATE POLYGON = [[86, 328], [844, 317], [1277, 520], [1273, 707], [125, 713]]
92
92
  zone_config: Optional[Dict[str, List[List[float]]]] = None #field(
@@ -116,25 +116,24 @@ class ColorDetectionConfig(BaseConfig):
116
116
  errors.append("smoothing_confidence_range_factor must be positive")
117
117
  return errors
118
118
 
119
- def __post_init__(self):
120
- # Lazy initialization: the ClipProcessor will be created once by the use case
121
- # to avoid repeated model downloads and to ensure GPU session reuse.
122
- # log_file = open("pip_jetson_bt.log", "w")
123
- # cmd = ["pip", "install", "--force-reinstall", "huggingface_hub", "regex", "safetensors"]
124
- # subprocess.Popen(
125
- # cmd,
126
- # stdout=log_file,
127
- # stderr=subprocess.STDOUT,
128
- # preexec_fn=os.setpgrp
129
- # )
130
- print("Came to post_init and libraries installed!!!")
131
- if self.detector:
132
- self.detector = ClipProcessor()
133
- print("ClipProcessor Loaded Successfully!!")
134
- else:
135
- print("Clip color detector disabled by config")
136
- self.detector = None
137
-
119
+ # def __post_init__(self):
120
+ # # Lazy initialization: the ClipProcessor will be created once by the use case
121
+ # # to avoid repeated model downloads and to ensure GPU session reuse.
122
+ # # log_file = open("pip_jetson_bt.log", "w")
123
+ # # cmd = ["pip", "install", "--force-reinstall", "huggingface_hub", "regex", "safetensors"]
124
+ # # subprocess.Popen(
125
+ # # cmd,
126
+ # # stdout=log_file,
127
+ # # stderr=subprocess.STDOUT,
128
+ # # preexec_fn=os.setpgrp
129
+ # # )
130
+ # print("Came to post_init and libraries installed!!!")
131
+ # if self.detector:
132
+ # self.detector = ClipProcessor()
133
+ # print("ClipProcessor Loaded Successfully!!")
134
+ # else:
135
+ # print("Clip color detector disabled by config")
136
+ # self.detector = None
138
137
 
139
138
 
140
139
  class ColorDetectionUseCase(BaseProcessor):
@@ -182,7 +181,7 @@ class ColorDetectionUseCase(BaseProcessor):
182
181
  self._zone_current_counts = {} # zone_name -> current count in zone
183
182
  self._zone_total_counts = {} # zone_name -> total count that have been in zone
184
183
  self.logger.info("Initialized ColorDetectionUseCase with tracking")
185
- #self.detector = None
184
+ self.detector = None # Will be initialized on first use
186
185
  self.all_color_data = {}
187
186
  self.all_color_counts = {}
188
187
  self.total_category_count = {}
@@ -294,30 +293,29 @@ class ColorDetectionUseCase(BaseProcessor):
294
293
  if config.zone_config:
295
294
  color_processed_data = self._is_in_zone_robust(color_processed_data,config.zone_config)
296
295
  print(color_processed_data)
296
+
297
+ # Initialize detector lazily on first use if enabled
297
298
  try:
298
299
  print("About to call process_color_in_frame...")
299
-
300
- if config.detector is None:
301
- print("ERROR: Detector is None after initialization attempt!")
300
+
301
+ if config.enable_detector and self.detector is None:
302
+ print("Initializing ClipProcessor for color detection...")
303
+ try:
304
+ self.detector = ClipProcessor()
305
+ print("ClipProcessor loaded successfully!")
306
+ logger.info("ClipProcessor loaded successfully!")
307
+ except Exception as init_error:
308
+ print(f"Failed to initialize ClipProcessor: {init_error}")
309
+ logger.error(f"Failed to initialize ClipProcessor: {init_error}")
310
+ self.detector = None
311
+
312
+ if self.detector is None:
313
+ print("Detector is disabled or failed to initialize, skipping color detection")
314
+ logger.warning("Detector is disabled or failed to initialize, skipping color detection")
302
315
  curr_frame_color = {}
303
-
304
- # else:
305
- # if color_processed_data:
306
- # t_id = color_processed_data[0].get('track_id')
307
- # if t_id is not None and t_id not in self.all_color_data:
308
- # # curr_frame_color = {}
309
- # curr_frame_color = config.detector.process_color_in_frame(color_processed_data,input_bytes,config.zone_config,stream_info)
310
- # res_dict[curr_frame_color[t_id]['color']] = curr_frame_color[t_id]['confidence']
311
- # else:
312
- # curr_frame_color = {}
313
- # print("process_color_in_frame completed successfully")
314
- # else:
315
- # curr_frame_color = {}
316
-
317
- #------------------------ORiginal Code to run on all frames-----------------------
318
316
  else:
319
317
  print(len(color_processed_data))
320
- curr_frame_color = config.detector.process_color_in_frame(
318
+ curr_frame_color = self.detector.process_color_in_frame(
321
319
  color_processed_data,
322
320
  input_bytes,
323
321
  config.zone_config,
@@ -36,24 +36,22 @@ print(f"Python version: {major_version}.{minor_version}")
36
36
  os.environ["ORT_LOG_SEVERITY_LEVEL"] = "3"
37
37
 
38
38
 
39
- _ENABLE_OCR_BOOTSTRAP = os.getenv("MATRICE_ENABLE_OCR_BOOTSTRAP", "0")
40
- if _ENABLE_OCR_BOOTSTRAP == "1":
41
- try:
42
- from ..ocr.fast_plate_ocr_py38 import LicensePlateRecognizer
43
- except:
44
- class LicensePlateRecognizer:
45
- """Stub fallback when fast_plate_ocr is not installed."""
46
- def __init__(self, *args, **kwargs):
47
- print("fast_plate_ocr from the local library py_inferenceis required for LicensePlateMonitorUseCase as platform is determined as python3.8 Jetson")
48
- else:
39
+ # Try to import LicensePlateRecognizer from local repo first, then installed package
40
+ _OCR_IMPORT_SOURCE = None
41
+ try:
42
+ from ..ocr.fast_plate_ocr_py38 import LicensePlateRecognizer
43
+ _OCR_IMPORT_SOURCE = "local_repo"
44
+ except ImportError:
49
45
  try:
50
46
  from fast_plate_ocr import LicensePlateRecognizer # type: ignore
51
- except: # pragma: no cover – optional dependency may be absent
52
- logging.error("fast_plate_ocr is required for LicensePlateMonitorUseCase but is not installed.")
47
+ _OCR_IMPORT_SOURCE = "installed_package"
48
+ except ImportError:
49
+ # Use stub class if neither import works
50
+ _OCR_IMPORT_SOURCE = "stub"
53
51
  class LicensePlateRecognizer: # type: ignore
54
- """Stub fallback when fast_plate_ocr is not installed."""
52
+ """Stub fallback when fast_plate_ocr is not available."""
55
53
  def __init__(self, *args, **kwargs):
56
- print("fast_plate_ocr is required for LicensePlateMonitorUseCase but is not installed.")
54
+ pass # Silent stub - error will be logged once during initialization
57
55
 
58
56
  # Internal utilities that are still required
59
57
  from ..ocr.preprocessing import ImagePreprocessor
@@ -124,23 +122,29 @@ class LicensePlateMonitorLogger:
124
122
 
125
123
  def initialize_session(self, config: LicensePlateMonitorConfig) -> None:
126
124
  """Initialize session and fetch server connection info if lpr_server_id is provided."""
125
+ self.logger.info("Initializing LicensePlateMonitorLogger session...")
126
+
127
127
  # Use existing session if provided, otherwise create new one
128
128
  if self.session:
129
+ self.logger.info("Session already initialized, skipping initialization")
129
130
  return
130
131
  if config.session:
131
132
  self.session = config.session
133
+ self.logger.info("Using provided session from config")
132
134
  if not self.session:
133
135
  # Initialize Matrice session
134
136
  if not HAS_MATRICE_SESSION:
137
+ self.logger.error("Matrice session module not available")
135
138
  raise ImportError("Matrice session is required for License Plate Monitoring")
136
139
  try:
140
+ self.logger.info("Creating new Matrice session...")
137
141
  self.session = Session(
138
142
  account_number=os.getenv("MATRICE_ACCOUNT_NUMBER", ""),
139
143
  access_key=os.getenv("MATRICE_ACCESS_KEY_ID", ""),
140
144
  secret_key=os.getenv("MATRICE_SECRET_ACCESS_KEY", ""),
141
145
  project_id=os.getenv("MATRICE_PROJECT_ID", ""),
142
146
  )
143
- self.logger.info("Initialized new Matrice session for License Plate Monitoring")
147
+ self.logger.info("Successfully initialized new Matrice session for License Plate Monitoring")
144
148
  except Exception as e:
145
149
  self.logger.error(f"Failed to initialize Matrice session: {e}", exc_info=True)
146
150
  raise
@@ -148,6 +152,7 @@ class LicensePlateMonitorLogger:
148
152
  # Fetch server connection info if lpr_server_id is provided
149
153
  if config.lpr_server_id:
150
154
  self.lpr_server_id = config.lpr_server_id
155
+ self.logger.info(f"Fetching LPR server connection info for server ID: {self.lpr_server_id}")
151
156
  try:
152
157
  self.server_info = self.get_server_connection_info()
153
158
  if self.server_info:
@@ -158,23 +163,26 @@ class LicensePlateMonitorLogger:
158
163
 
159
164
  if server_host == self.public_ip:
160
165
  self.server_base_url = f"http://localhost:{server_port}"
161
- self.logger.warning(f"Server host matches public IP, using localhost: {self.server_base_url}")
166
+ self.logger.info(f"Server host matches public IP ({self.public_ip}), using localhost: {self.server_base_url}")
162
167
  else:
163
168
  self.server_base_url = f"https://{server_host}:{server_port}"
164
- self.logger.warning(f"LPR server base URL: {self.server_base_url}")
169
+ self.logger.info(f"LPR server base URL configured: {self.server_base_url}")
165
170
 
166
171
  self.session.update(self.server_info.get('projectID', ''))
167
172
  self.logger.info(f"Updated Matrice session with project ID: {self.server_info.get('projectID', '')}")
168
173
  else:
169
- self.logger.warning("Failed to fetch LPR server connection info")
174
+ self.logger.warning("Failed to fetch LPR server connection info - server_info is None")
170
175
  except Exception as e:
171
176
  self.logger.error(f"Error fetching LPR server connection info: {e}", exc_info=True)
177
+ else:
178
+ self.logger.info("No lpr_server_id provided in config, skipping server connection info fetch")
172
179
 
173
180
  def _get_public_ip(self) -> str:
174
181
  """Get the public IP address of this machine."""
182
+ self.logger.info("Fetching public IP address...")
175
183
  try:
176
184
  public_ip = urllib.request.urlopen("https://v4.ident.me", timeout=120).read().decode("utf8").strip()
177
- self.logger.warning(f"Successfully fetched external IP: {public_ip}")
185
+ self.logger.info(f"Successfully fetched external IP: {public_ip}")
178
186
  return public_ip
179
187
  except Exception as e:
180
188
  self.logger.error(f"Error fetching external IP: {e}", exc_info=True)
@@ -183,10 +191,15 @@ class LicensePlateMonitorLogger:
183
191
  def get_server_connection_info(self) -> Optional[Dict[str, Any]]:
184
192
  """Fetch server connection info from RPC."""
185
193
  if not self.lpr_server_id:
194
+ self.logger.warning("No lpr_server_id set, cannot fetch server connection info")
186
195
  return None
187
196
 
188
197
  try:
189
- response = self.session.rpc.get(f"/v1/actions/lpr_servers/{self.lpr_server_id}")
198
+ endpoint = f"/v1/actions/lpr_servers/{self.lpr_server_id}"
199
+ self.logger.info(f"Sending GET request to: {endpoint}")
200
+ response = self.session.rpc.get(endpoint)
201
+ self.logger.info(f"Received response: success={response.get('success')}, code={response.get('code')}, message={response.get('message')}")
202
+
190
203
  if response.get("success", False) and response.get("code") == 200:
191
204
  # Response format:
192
205
  # {'success': True,
@@ -202,7 +215,9 @@ class LicensePlateMonitorLogger:
202
215
  # 'projectID': '68ca6372ab79ba13ef699ba6',
203
216
  # 'region': 'United States',
204
217
  # 'isShared': False}}
205
- return response.get("data", {})
218
+ data = response.get("data", {})
219
+ self.logger.info(f"Server connection info retrieved: name={data.get('name')}, host={data.get('host')}, port={data.get('port')}, status={data.get('status')}")
220
+ return data
206
221
  else:
207
222
  self.logger.warning(f"Failed to fetch server info: {response.get('message', 'Unknown error')}")
208
223
  return None
@@ -214,20 +229,69 @@ class LicensePlateMonitorLogger:
214
229
  """Check if enough time has passed since last log for this plate."""
215
230
  current_time = time.time()
216
231
  last_log_time = self.plate_log_timestamps.get(plate_text, 0)
232
+ time_since_last_log = current_time - last_log_time
217
233
 
218
- if current_time - last_log_time >= cooldown:
234
+ if time_since_last_log >= cooldown:
235
+ self.logger.debug(f"Plate {plate_text} ready to log (last logged {time_since_last_log:.1f}s ago, cooldown={cooldown}s)")
219
236
  return True
220
- return False
237
+ else:
238
+ self.logger.debug(f"Plate {plate_text} in cooldown period ({time_since_last_log:.1f}s elapsed, {cooldown - time_since_last_log:.1f}s remaining)")
239
+ return False
221
240
 
222
241
  def update_log_timestamp(self, plate_text: str) -> None:
223
242
  """Update the last log timestamp for a plate."""
224
243
  self.plate_log_timestamps[plate_text] = time.time()
244
+ self.logger.debug(f"Updated log timestamp for plate: {plate_text}")
245
+
246
+ def _format_timestamp_rfc3339(self, timestamp: str) -> str:
247
+ """Convert timestamp to RFC3339 format (2006-01-02T15:04:05Z).
248
+
249
+ Handles various input formats:
250
+ - "YYYY-MM-DD-HH:MM:SS.ffffff UTC"
251
+ - "YYYY:MM:DD HH:MM:SS"
252
+ - Unix timestamp (float/int)
253
+ """
254
+ try:
255
+ # If already in RFC3339 format, return as is
256
+ if 'T' in timestamp and timestamp.endswith('Z'):
257
+ return timestamp
258
+
259
+ # Try to parse common formats
260
+ dt = None
261
+
262
+ # Format: "2025-08-19-04:22:47.187574 UTC"
263
+ if '-' in timestamp and 'UTC' in timestamp:
264
+ timestamp_clean = timestamp.replace(' UTC', '')
265
+ dt = datetime.strptime(timestamp_clean, '%Y-%m-%d-%H:%M:%S.%f')
266
+ # Format: "2025:10:23 14:30:45"
267
+ elif ':' in timestamp and ' ' in timestamp:
268
+ dt = datetime.strptime(timestamp, '%Y:%m:%d %H:%M:%S')
269
+ # Format: numeric timestamp
270
+ elif timestamp.replace('.', '').isdigit():
271
+ dt = datetime.fromtimestamp(float(timestamp), tz=timezone.utc)
272
+
273
+ if dt is None:
274
+ # Fallback to current time
275
+ dt = datetime.now(timezone.utc)
276
+ else:
277
+ # Ensure timezone is UTC
278
+ if dt.tzinfo is None:
279
+ dt = dt.replace(tzinfo=timezone.utc)
280
+
281
+ # Format to RFC3339: 2006-01-02T15:04:05Z
282
+ return dt.strftime('%Y-%m-%dT%H:%M:%SZ')
283
+
284
+ except Exception as e:
285
+ self.logger.warning(f"Failed to parse timestamp '{timestamp}': {e}. Using current time.")
286
+ return datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
225
287
 
226
288
  async def log_plate(self, plate_text: str, timestamp: str, stream_info: Dict[str, Any], cooldown: float = 30.0) -> bool:
227
289
  """Log plate to RPC server with cooldown period."""
290
+ self.logger.info(f"Attempting to log plate: {plate_text} at {timestamp}")
291
+
228
292
  # Check cooldown
229
293
  if not self.should_log_plate(plate_text, cooldown):
230
- self.logger.debug(f"Plate {plate_text} skipped due to cooldown period")
294
+ self.logger.info(f"Plate {plate_text} NOT SENT - skipped due to cooldown period")
231
295
  return False
232
296
 
233
297
  try:
@@ -236,23 +300,36 @@ class LicensePlateMonitorLogger:
236
300
  location = camera_info.get("location", "")
237
301
  frame_id = stream_info.get("frame_id", "")
238
302
 
303
+ # Get project ID from server_info
304
+ project_id = self.server_info.get('projectID', '') if self.server_info else ''
305
+
306
+ # Format timestamp to RFC3339 format (2006-01-02T15:04:05Z)
307
+ rfc3339_timestamp = self._format_timestamp_rfc3339(timestamp)
308
+
239
309
  payload = {
240
310
  'licensePlate': plate_text,
241
311
  'frameId': frame_id,
242
312
  'location': location,
243
313
  'camera': camera_name,
244
- 'captureTimestamp': timestamp
314
+ 'captureTimestamp': rfc3339_timestamp,
315
+ 'projectId': project_id
245
316
  }
246
317
 
247
- await self.session.rpc.post_async('/v1/lpr-server/detections', payload=payload, base_url=self.server_base_url)
318
+ # Add projectId as query parameter
319
+ endpoint = f'/v1/lpr-server/detections?projectId={project_id}'
320
+ self.logger.info(f"Sending POST request to {self.server_base_url}{endpoint} with payload: {payload}")
321
+
322
+ response = await self.session.rpc.post_async(endpoint, payload=payload, base_url=self.server_base_url)
323
+
324
+ self.logger.info(f"API Response received for plate {plate_text}: {response}")
248
325
 
249
326
  # Update timestamp after successful log
250
327
  self.update_log_timestamp(plate_text)
251
- self.logger.info(f"Successfully logged plate: {plate_text} at {timestamp}")
328
+ self.logger.info(f"Plate {plate_text} SUCCESSFULLY SENT and logged at {rfc3339_timestamp}")
252
329
  return True
253
330
 
254
331
  except Exception as e:
255
- self.logger.error(f"Failed to log plate {plate_text}: {e}", exc_info=True)
332
+ self.logger.error(f"Plate {plate_text} NOT SENT - Failed to log: {e}", exc_info=True)
256
333
  return False
257
334
 
258
335
  class LicensePlateMonitorUseCase(BaseProcessor):
@@ -296,11 +373,19 @@ class LicensePlateMonitorUseCase(BaseProcessor):
296
373
  self.image_preprocessor = ImagePreprocessor()
297
374
  # Fast OCR model (shared across instances)
298
375
  if LicensePlateMonitorUseCase._ocr_model is None:
299
- try:
376
+ if _OCR_IMPORT_SOURCE == "stub":
377
+ # Using stub - log warning once
378
+ self.logger.error("OCR module not available. LicensePlateRecognizer will not function. Install: pip install fast-plate-ocr[onnx]")
300
379
  LicensePlateMonitorUseCase._ocr_model = LicensePlateRecognizer('cct-s-v1-global-model')
301
- self.logger.info("LicensePlateRecognizer loaded successfully")
302
- except Exception as e:
303
- self.logger.warning(f"Failed to initialise LicensePlateRecognizer: {e}")
380
+ else:
381
+ # Try to load real OCR model
382
+ try:
383
+ LicensePlateMonitorUseCase._ocr_model = LicensePlateRecognizer('cct-s-v1-global-model')
384
+ source_msg = "from local repo" if _OCR_IMPORT_SOURCE == "local_repo" else "from installed package"
385
+ self.logger.info(f"LicensePlateRecognizer loaded successfully {source_msg}")
386
+ except Exception as e:
387
+ self.logger.error(f"Failed to initialize LicensePlateRecognizer: {e}", exc_info=True)
388
+ LicensePlateMonitorUseCase._ocr_model = None
304
389
  self.ocr_model = LicensePlateMonitorUseCase._ocr_model
305
390
  # OCR text history for stability checks (text → consecutive frame count)
306
391
  self._text_history: Dict[str, int] = {}
@@ -419,11 +504,6 @@ class LicensePlateMonitorUseCase(BaseProcessor):
419
504
  if not input_bytes:
420
505
  return self.create_error_result("input_bytes (video/image) is required for license plate monitoring",
421
506
  usecase=self.name, category=self.category, context=context)
422
-
423
- print("--------------------------------------")
424
- print("config.alert_config",config.alert_config)
425
- print(config)
426
- print("--------------------------------------")
427
507
 
428
508
  # Initialize plate logger if lpr_server_id is provided (optional flow)
429
509
  if config.lpr_server_id and self._logging_enabled:
@@ -704,15 +784,17 @@ class LicensePlateMonitorUseCase(BaseProcessor):
704
784
 
705
785
  def _run_ocr(self, crop: np.ndarray) -> str:
706
786
  """Run OCR on a cropped plate image and return cleaned text or empty string."""
707
- # print("---------OCR CROP22",crop)
708
- # print("---------OCR CROP SIZE22",crop.size)
709
-
710
787
  if crop is None or crop.size == 0 or self.ocr_model is None:
711
788
  return ""
789
+
790
+ # Check if we have a valid OCR model (not the stub) - silently return empty if stub
791
+ if not hasattr(self.ocr_model, 'run'):
792
+ return ""
793
+
712
794
  try:
713
- # fast_plate_ocr expects RGB
714
- #rgb_crop = cv2.cvtColor(crop, cv2.COLOR_BGR2RGB)
795
+ # fast_plate_ocr LicensePlateRecognizer has a run() method
715
796
  res = self.ocr_model.run(crop)
797
+
716
798
  if isinstance(res, list):
717
799
  res = res[0] if res else ""
718
800
  cleaned_text = self._clean_text(str(res))
@@ -723,12 +805,14 @@ class LicensePlateMonitorUseCase(BaseProcessor):
723
805
  response = all(ch.isalpha() for ch in cleaned_text)
724
806
  elif self._ocr_mode == "alphanumeric":
725
807
  response = True
808
+ else:
809
+ response = False
726
810
 
727
811
  if response:
728
812
  return cleaned_text
729
- else:
730
- return ""
813
+ return ""
731
814
  except Exception as exc:
815
+ # Only log at debug level to avoid spam
732
816
  self.logger.warning(f"OCR failed: {exc}")
733
817
  return ""
734
818
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrice_analytics
3
- Version: 0.1.31
3
+ Version: 0.1.32
4
4
  Summary: Common server utilities for Matrice.ai services
5
5
  Author-email: "Matrice.ai" <dipendra@matrice.ai>
6
6
  License-Expression: MIT
@@ -12,8 +12,8 @@ matrice_analytics/boundary_drawing_internal/usage/boundary_drawer_launcher.py,sh
12
12
  matrice_analytics/boundary_drawing_internal/usage/simple_boundary_launcher.py,sha256=jHPriRLorLuiC8km0MFNS96w121tKxd7t5GQl7I5kKE,3494
13
13
  matrice_analytics/post_processing/README.md,sha256=bDszazvqV5xbGhMM6hDaMctIyk5gox9bADo2IZZ9Goo,13368
14
14
  matrice_analytics/post_processing/__init__.py,sha256=dxGBUQaRCGndQmXpYAWqUhDeUZAcxU-_6HFnm3GRDRA,29417
15
- matrice_analytics/post_processing/config.py,sha256=p0BHD4XM5jFueibW7cfusrA4OMHP2_9PD9lNvTxtaiI,6569
16
- matrice_analytics/post_processing/post_processor.py,sha256=meE4fDlF6F7SEwLrSOm3cfBmER2QUa2XCU_xyzqHzzc,43944
15
+ matrice_analytics/post_processing/config.py,sha256=V0s86qNapyDE6Q81ZS_1uzNqAjz-vc5L-9Tb33XaLEo,6771
16
+ matrice_analytics/post_processing/post_processor.py,sha256=ql4WuT1qVusXXYP36PNICQYPgvWOwBBErAb4UBMXlm8,44344
17
17
  matrice_analytics/post_processing/advanced_tracker/README.md,sha256=RM8dynVoUWKn_hTbw9c6jHAbnQj-8hEAXnmuRZr2w1M,22485
18
18
  matrice_analytics/post_processing/advanced_tracker/__init__.py,sha256=tAPFzI_Yep5TLX60FDwKqBqppc-EbxSr0wNsQ9DGI1o,423
19
19
  matrice_analytics/post_processing/advanced_tracker/base.py,sha256=VqWy4dd5th5LK-JfueTt2_GSEoOi5QQfQxjTNhmQoLc,3580
@@ -24,14 +24,14 @@ matrice_analytics/post_processing/advanced_tracker/strack.py,sha256=OSai-SSpC9_u
24
24
  matrice_analytics/post_processing/advanced_tracker/tracker.py,sha256=yN_tUzDZ-8M4NSoZrKf0OW0JA0JxOvz2oYKgbm-os88,15687
25
25
  matrice_analytics/post_processing/core/__init__.py,sha256=QlgoJwjTU-3UYTEmFRN6wFWpOr7zNSnrohoqLBF5bNY,1434
26
26
  matrice_analytics/post_processing/core/base.py,sha256=V6iF2_u8APBKIeudNBUP-_cHL4FAYrgEpM7W7fbtT74,29112
27
- matrice_analytics/post_processing/core/config.py,sha256=20RGCpqo7RDIW0fzI2p4i_Yb5lldddmtELRwaNdKFnY,127079
27
+ matrice_analytics/post_processing/core/config.py,sha256=uyxWndO-DE9PeGD_h5K3TeB0AUgGa5JOpgXNAMEKj6s,131528
28
28
  matrice_analytics/post_processing/core/config_utils.py,sha256=QuAS-_JKSoNOtfUWgr7Alf_wsqODzN2rHlQu-cHRK0s,34311
29
29
  matrice_analytics/post_processing/face_reg/__init__.py,sha256=yntaiGlW9vdjBpPZQXNuovALihJPzRlFyUE88l3MhBA,1364
30
30
  matrice_analytics/post_processing/face_reg/compare_similarity.py,sha256=NlFc8b2a74k0PqSFAbuM_fUbA1BT3pr3VUgvSqRpJzQ,23396
31
- matrice_analytics/post_processing/face_reg/embedding_manager.py,sha256=2n7GR8D9SklCf3LVjaHkQAwH9psw2wFI2iNjA0bWisU,31487
32
- matrice_analytics/post_processing/face_reg/face_recognition.py,sha256=aGL8dsK2D63K4Z-yXbSuM4bd8v6e_F7rlXRUujoaZoE,89866
33
- matrice_analytics/post_processing/face_reg/face_recognition_client.py,sha256=lPCnmd_W-euRaYsh2vJlkO7toBngHZrTUTFqkkKuy38,27549
34
- matrice_analytics/post_processing/face_reg/people_activity_logging.py,sha256=n6yV6WgFKMGM1fJZcTpliMap8wKyWv5t8dGOk7Dymvw,11894
31
+ matrice_analytics/post_processing/face_reg/embedding_manager.py,sha256=qbh0df3-YbE0qvFDQvjpCg-JrsCZRJ5capjQ2LPOj1k,35619
32
+ matrice_analytics/post_processing/face_reg/face_recognition.py,sha256=17RVs_5BtGw2ruksL2JLbpXljbs4-96tP-yKGX3hrOI,90232
33
+ matrice_analytics/post_processing/face_reg/face_recognition_client.py,sha256=jPssMC_UuKn8M-9IaTirP-aswpcTiYV5vdTImt6lt4Q,27576
34
+ matrice_analytics/post_processing/face_reg/people_activity_logging.py,sha256=NhJXy9jCy_mlZiDXv8IeGyrRN_TL0kKCe3ZOlaNbZUw,13676
35
35
  matrice_analytics/post_processing/ocr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
36
  matrice_analytics/post_processing/ocr/easyocr_extractor.py,sha256=FwVoUATYdiZtfhSAoiyCo_9dgA786pFZfONx6tsQOfE,11403
37
37
  matrice_analytics/post_processing/ocr/postprocessing.py,sha256=RILArp8I9WRH7bALVZ9wPGc-aR7YMdqV1ndOOIcOnGQ,12309
@@ -100,7 +100,7 @@ matrice_analytics/post_processing/usecases/cardiomegaly_classification.py,sha256
100
100
  matrice_analytics/post_processing/usecases/cell_microscopy_segmentation.py,sha256=eQ_s5u3Vnvja6-FmI6ZPxlNkaZtG-pVjTu8NuLjZJ5M,43714
101
101
  matrice_analytics/post_processing/usecases/chicken_pose_detection.py,sha256=-e8di7Am-E-FCQFrSY8qJTO1aWtdRAVJoE-VKBgcyyI,29291
102
102
  matrice_analytics/post_processing/usecases/child_monitoring.py,sha256=z3oymoqq4hDGwA8MkdEONZW_Vx5CAZmvzZaNLsqmCfw,39380
103
- matrice_analytics/post_processing/usecases/color_detection.py,sha256=d9Gg61Wj5r-QGNtok4u5Z_uZ5VYsamU0Uqer7LgR2hY,90408
103
+ matrice_analytics/post_processing/usecases/color_detection.py,sha256=K3jemPgPs5AzYDKeI_3dRmH_nhqN96tvEE9wsw5bPIs,90498
104
104
  matrice_analytics/post_processing/usecases/color_map_utils.py,sha256=SP-AEVcjLmL8rxblu-ixqUJC2fqlcr7ab4hWo4Fcr_k,2677
105
105
  matrice_analytics/post_processing/usecases/concrete_crack_detection.py,sha256=pxhOH_hG4hq9yytNepbGMdk2W_lTG8D1_2RAagaPBkg,40252
106
106
  matrice_analytics/post_processing/usecases/crop_weed_detection.py,sha256=Ao1k5fJDYU_f6yZ8VO-jW8-esECV0-zY5Q570c_fako,35674
@@ -126,7 +126,7 @@ matrice_analytics/post_processing/usecases/leaf.py,sha256=cwgB1ZNxkQFtkk-thSJrkX
126
126
  matrice_analytics/post_processing/usecases/leaf_disease.py,sha256=bkiLccTdf4KUq3he4eCpBlKXb5exr-WBhQ_oWQ7os68,36225
127
127
  matrice_analytics/post_processing/usecases/leak_detection.py,sha256=oOCLLVMuXVeXPHyN8FUrD3U9JYJJwIz-5fcEMgvLdls,40531
128
128
  matrice_analytics/post_processing/usecases/license_plate_detection.py,sha256=dsavd92-wnyXCNrCzaRj24zH7BVvLSa09HkYsrOXYDM,50806
129
- matrice_analytics/post_processing/usecases/license_plate_monitoring.py,sha256=FqVEqpNbFr5yxcx26jns_iyPyIIWiewoC6A8bLRQnPQ,70716
129
+ matrice_analytics/post_processing/usecases/license_plate_monitoring.py,sha256=h4RHX9HXR6W0eQfFE1G6FRygvlmZQAWN3H323FWqvlQ,75268
130
130
  matrice_analytics/post_processing/usecases/litter_monitoring.py,sha256=XaHAUGRBDJg_iVbu8hRMjTR-5TqrLj6ZNCRkInbzZTY,33255
131
131
  matrice_analytics/post_processing/usecases/mask_detection.py,sha256=L_s6ZiT5zeXG-BsFcskb3HEG98DhLgqeMSDmCuwOteU,41501
132
132
  matrice_analytics/post_processing/usecases/natural_disaster.py,sha256=ehxdPBoYcZWGVDOVn_mHFoz4lIE8LrveAkuXQj0n9XE,44253
@@ -188,8 +188,8 @@ matrice_analytics/post_processing/utils/format_utils.py,sha256=UTF7A5h9j0_S12xH9
188
188
  matrice_analytics/post_processing/utils/geometry_utils.py,sha256=BWfdM6RsdJTTLR1GqkWfdwpjMEjTCJyuBxA4zVGKdfk,9623
189
189
  matrice_analytics/post_processing/utils/smoothing_utils.py,sha256=78U-yucAcjUiZ0NIAc9NOUSIT0PWP1cqyIPA_Fdrjp0,14699
190
190
  matrice_analytics/post_processing/utils/tracking_utils.py,sha256=rWxuotnJ3VLMHIBOud2KLcu4yZfDp7hVPWUtNAq_2xw,8288
191
- matrice_analytics-0.1.31.dist-info/licenses/LICENSE.txt,sha256=_uQUZpgO0mRYL5-fPoEvLSbNnLPv6OmbeEDCHXhK6Qc,1066
192
- matrice_analytics-0.1.31.dist-info/METADATA,sha256=mbvw2ctIVZ0OR5eg_tmW8pjWlFCyh3hFaxCTYtJ1jyE,14378
193
- matrice_analytics-0.1.31.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
194
- matrice_analytics-0.1.31.dist-info/top_level.txt,sha256=STAPEU-e-rWTerXaspdi76T_eVRSrEfFpURSP7_Dt8E,18
195
- matrice_analytics-0.1.31.dist-info/RECORD,,
191
+ matrice_analytics-0.1.32.dist-info/licenses/LICENSE.txt,sha256=_uQUZpgO0mRYL5-fPoEvLSbNnLPv6OmbeEDCHXhK6Qc,1066
192
+ matrice_analytics-0.1.32.dist-info/METADATA,sha256=zSaINlI-8xwx56Oc1aLW6qnbechmTBUIp1ZLZ0mKqJQ,14378
193
+ matrice_analytics-0.1.32.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
194
+ matrice_analytics-0.1.32.dist-info/top_level.txt,sha256=STAPEU-e-rWTerXaspdi76T_eVRSrEfFpURSP7_Dt8E,18
195
+ matrice_analytics-0.1.32.dist-info/RECORD,,