matrice-analytics 0.1.60__py3-none-any.whl → 0.1.89__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. matrice_analytics/post_processing/config.py +2 -2
  2. matrice_analytics/post_processing/core/base.py +1 -1
  3. matrice_analytics/post_processing/face_reg/embedding_manager.py +8 -8
  4. matrice_analytics/post_processing/face_reg/face_recognition.py +886 -201
  5. matrice_analytics/post_processing/face_reg/face_recognition_client.py +68 -2
  6. matrice_analytics/post_processing/usecases/advanced_customer_service.py +908 -498
  7. matrice_analytics/post_processing/usecases/color_detection.py +18 -18
  8. matrice_analytics/post_processing/usecases/customer_service.py +356 -9
  9. matrice_analytics/post_processing/usecases/fire_detection.py +149 -11
  10. matrice_analytics/post_processing/usecases/license_plate_monitoring.py +548 -40
  11. matrice_analytics/post_processing/usecases/people_counting.py +11 -11
  12. matrice_analytics/post_processing/usecases/vehicle_monitoring.py +34 -34
  13. matrice_analytics/post_processing/usecases/weapon_detection.py +98 -22
  14. matrice_analytics/post_processing/utils/alert_instance_utils.py +950 -0
  15. matrice_analytics/post_processing/utils/business_metrics_manager_utils.py +1245 -0
  16. matrice_analytics/post_processing/utils/incident_manager_utils.py +1657 -0
  17. {matrice_analytics-0.1.60.dist-info → matrice_analytics-0.1.89.dist-info}/METADATA +1 -1
  18. {matrice_analytics-0.1.60.dist-info → matrice_analytics-0.1.89.dist-info}/RECORD +21 -18
  19. {matrice_analytics-0.1.60.dist-info → matrice_analytics-0.1.89.dist-info}/WHEEL +0 -0
  20. {matrice_analytics-0.1.60.dist-info → matrice_analytics-0.1.89.dist-info}/licenses/LICENSE.txt +0 -0
  21. {matrice_analytics-0.1.60.dist-info → matrice_analytics-0.1.89.dist-info}/top_level.txt +0 -0
@@ -18,6 +18,8 @@ from ..utils import (
18
18
  BBoxSmoothingConfig,
19
19
  BBoxSmoothingTracker
20
20
  )
21
+ # Import alert system utilities
22
+ from ..utils.alert_instance_utils import ALERT_INSTANCE
21
23
  # External dependencies
22
24
  import cv2
23
25
  import numpy as np
@@ -31,11 +33,15 @@ import asyncio
31
33
  import urllib
32
34
  import urllib.request
33
35
  import base64
36
+ from pathlib import Path
34
37
  # Get the major and minor version numbers
35
38
  major_version = sys.version_info.major
36
39
  minor_version = sys.version_info.minor
37
40
  print(f"Python version: {major_version}.{minor_version}")
38
41
  os.environ["ORT_LOG_SEVERITY_LEVEL"] = "3"
42
+ import base64
43
+ from matrice_common.stream.matrice_stream import MatriceStream, StreamType
44
+ from matrice_common.session import Session
39
45
 
40
46
 
41
47
  # Lazy import mechanism for LicensePlateRecognizer
@@ -129,7 +135,6 @@ from ..ocr.preprocessing import ImagePreprocessor
129
135
  from ..core.config import BaseConfig, AlertConfig, ZoneConfig
130
136
 
131
137
  try:
132
- from matrice_common.session import Session
133
138
  HAS_MATRICE_SESSION = True
134
139
  except ImportError:
135
140
  HAS_MATRICE_SESSION = False
@@ -156,6 +161,7 @@ class LicensePlateMonitorConfig(BaseConfig):
156
161
  ocr_mode:str = field(default_factory=lambda: "numeric") # "alphanumeric" or "numeric" or "alphabetic"
157
162
  session: Optional[Session] = None
158
163
  lpr_server_id: Optional[str] = None # Optional LPR server ID for remote logging
164
+ redis_server_id: Optional[str] = None # Optional Redis server ID for instant alerts
159
165
  plate_log_cooldown: float = 30.0 # Cooldown period in seconds for logging same plate
160
166
 
161
167
  def validate(self) -> List[str]:
@@ -237,6 +243,7 @@ class LicensePlateMonitorLogger:
237
243
  # Fetch server connection info if lpr_server_id is provided
238
244
  if config.lpr_server_id:
239
245
  self.lpr_server_id = config.lpr_server_id
246
+ self.logger.info(f"[LP_LOGGING] CONFIG PRINTTEST: {config}")
240
247
  self.logger.info(f"[LP_LOGGING] Fetching LPR server connection info for server ID: {self.lpr_server_id}")
241
248
  try:
242
249
  self.server_info = self.get_server_connection_info()
@@ -265,6 +272,7 @@ class LicensePlateMonitorLogger:
265
272
  self.logger.error("[LP_LOGGING] Failed to fetch LPR server connection info - server_info is None")
266
273
  self.logger.error("[LP_LOGGING] This will prevent plate logging from working!")
267
274
  except Exception as e:
275
+ #pass
268
276
  self.logger.error(f"[LP_LOGGING] Error fetching LPR server connection info: {e}", exc_info=True)
269
277
  self.logger.error("[LP_LOGGING] This will prevent plate logging from working!")
270
278
  else:
@@ -284,6 +292,17 @@ class LicensePlateMonitorLogger:
284
292
  self.logger.error(f"Error fetching external IP: {e}", exc_info=True)
285
293
  return "localhost"
286
294
 
295
+ def _get_backend_base_url(self) -> str:
296
+ """Resolve backend base URL based on ENV variable: prod/staging/dev."""
297
+ env = os.getenv("ENV", "prod").strip().lower()
298
+ if env in ("prod", "production"):
299
+ host = "prod.backend.app.matrice.ai"
300
+ elif env in ("dev", "development"):
301
+ host = "dev.backend.app.matrice.ai"
302
+ else:
303
+ host = "staging.backend.app.matrice.ai"
304
+ return f"https://{host}"
305
+
287
306
  def get_server_connection_info(self) -> Optional[Dict[str, Any]]:
288
307
  """Fetch server connection info from RPC."""
289
308
  if not self.lpr_server_id:
@@ -406,16 +425,21 @@ class LicensePlateMonitorLogger:
406
425
  self.logger.info(f"[LP_LOGGING] ===== PLATE LOG REQUEST END (SKIPPED) =====")
407
426
  return False
408
427
 
428
+ if not stream_info:
429
+ self.logger.info(f"[LP_LOGGING] Stream info is None, skipping plate log")
430
+ stream_info = {}
431
+
409
432
  try:
410
433
  camera_info = stream_info.get("camera_info", {})
411
- camera_name = camera_info.get("camera_name", "")
412
- location = camera_info.get("location", "")
434
+ camera_name = camera_info.get("camera_name", "default_camera")
435
+ location = camera_info.get("location", "default_location")
413
436
  frame_id = stream_info.get("frame_id", "")
414
437
 
415
438
  print(f"[LP_LOGGING] Camera: '{camera_name}', Location: '{location}'")
416
439
  self.logger.info(f"[LP_LOGGING] Stream Info - Camera: '{camera_name}', Location: '{location}', Frame ID: '{frame_id}'")
417
440
 
418
441
  # Get project ID from server_info
442
+ self.logger.info(f"[LP_LOGGING] SERVER-INFO: '{self.server_info}'")
419
443
  project_id = self.server_info.get('projectID', '') if self.server_info else ''
420
444
  self.logger.info(f"[LP_LOGGING] Project ID: '{project_id}'")
421
445
 
@@ -438,7 +462,7 @@ class LicensePlateMonitorLogger:
438
462
  full_url = f"{self.server_base_url}{endpoint}"
439
463
  print(f"[LP_LOGGING] Sending POST to: {full_url}")
440
464
  self.logger.info(f"[LP_LOGGING] Sending POST request to: {full_url}")
441
- self.logger.info(f"[LP_LOGGING] Payload: licensePlate='{plate_text}', frameId='{frame_id}', location='{location}', camera='{camera_name}', imageData length={len(image_data) if image_data else 0}")
465
+ self.logger.info(f"[LP_LOGGING] Payload: licensePlate='{plate_text}', frameId='{frame_id}', location='{location}', camera='{camera_name}'")
442
466
 
443
467
  response = await self.session.rpc.post_async(endpoint, payload=payload, base_url=self.server_base_url)
444
468
 
@@ -509,9 +533,330 @@ class LicensePlateMonitorUseCase(BaseProcessor):
509
533
 
510
534
  # Initialize plate logger (optional, only used if lpr_server_id is provided)
511
535
  self.plate_logger: Optional[LicensePlateMonitorLogger] = None
512
- self._logging_enabled = True
536
+ self._logging_enabled = True # False //ToDo: DISABLED FOR NOW, ENABLED FOR PRODUCTION. ##
513
537
  self._plate_logger_initialized = False # Track if plate logger has been initialized
514
-
538
+
539
+ # Initialize instant alert manager (will be lazily initialized on first process() call)
540
+ self.alert_manager: Optional[ALERT_INSTANCE] = None
541
+ self._alert_manager_initialized = False # Track initialization to do it only once
542
+
543
+ def set_alert_manager(self, alert_manager: ALERT_INSTANCE) -> None:
544
+ """
545
+ Set the alert manager instance for instant alerts.
546
+
547
+ Args:
548
+ alert_manager: ALERT_INSTANCE instance configured with Redis/Kafka clients
549
+ """
550
+ self.alert_manager = alert_manager
551
+ self.logger.info("Alert manager set for license plate monitoring")
552
+
553
+ def _discover_action_id(self) -> Optional[str]:
554
+ """Discover action_id from current working directory name (and parents), similar to face_recognition flow."""
555
+ try:
556
+ import re as _re
557
+ pattern = _re.compile(r"^[0-9a-f]{8,}$", _re.IGNORECASE)
558
+ candidates: List[str] = []
559
+ try:
560
+ cwd = Path.cwd()
561
+ candidates.append(cwd.name)
562
+ for parent in cwd.parents:
563
+ candidates.append(parent.name)
564
+ except Exception:
565
+ pass
566
+
567
+ try:
568
+ usr_src = Path("/usr/src")
569
+ if usr_src.exists():
570
+ for child in usr_src.iterdir():
571
+ if child.is_dir():
572
+ candidates.append(child.name)
573
+ except Exception:
574
+ pass
575
+
576
+ for candidate in candidates:
577
+ if candidate and len(candidate) >= 8 and pattern.match(candidate):
578
+ return candidate
579
+ except Exception:
580
+ pass
581
+ return None
582
+
583
+ def _get_backend_base_url(self) -> str:
584
+ """Resolve backend base URL based on ENV variable: prod/staging/dev."""
585
+ env = os.getenv("ENV", "prod").strip().lower()
586
+ if env in ("prod", "production"):
587
+ host = "prod.backend.app.matrice.ai"
588
+ elif env in ("dev", "development"):
589
+ host = "dev.backend.app.matrice.ai"
590
+ else:
591
+ host = "staging.backend.app.matrice.ai"
592
+ return f"https://{host}"
593
+
594
+ def _mask_value(self, value: Optional[str]) -> str:
595
+ """Mask sensitive values for logging/printing."""
596
+ if not value:
597
+ return ""
598
+ if len(value) <= 4:
599
+ return "*" * len(value)
600
+ return value[:2] + "*" * (len(value) - 4) + value[-2:]
601
+
602
+ def _get_public_ip(self) -> str:
603
+ """Get the public IP address of this machine."""
604
+ self.logger.info("Fetching public IP address...")
605
+ try:
606
+ public_ip = urllib.request.urlopen("https://v4.ident.me", timeout=120).read().decode("utf8").strip()
607
+ #self.logger.info(f"Successfully fetched external IP: {public_ip}")
608
+ return public_ip
609
+ except Exception as e:
610
+ #self.logger.error(f"Error fetching external IP: {e}", exc_info=True)
611
+ return "localhost"
612
+
613
+ def _initialize_alert_manager_once(self, config: LicensePlateMonitorConfig) -> None:
614
+ """
615
+ Initialize alert manager ONCE with Redis OR Kafka clients (Environment based).
616
+ Called from process() on first invocation.
617
+ Uses config.session (existing session from pipeline).
618
+ """
619
+ if self._alert_manager_initialized:
620
+ return
621
+
622
+ try:
623
+ # Import required modules
624
+ import base64
625
+ from matrice_common.stream.matrice_stream import MatriceStream, StreamType
626
+
627
+ # Use existing session from config (same pattern as plate_logger)
628
+ if not config.session:
629
+ account_number = os.getenv("MATRICE_ACCOUNT_NUMBER", "")
630
+ access_key_id = os.getenv("MATRICE_ACCESS_KEY_ID", "")
631
+ secret_key = os.getenv("MATRICE_SECRET_ACCESS_KEY", "")
632
+ project_id = os.getenv("MATRICE_PROJECT_ID", "")
633
+
634
+ self.session = Session(
635
+ account_number=account_number,
636
+ access_key=access_key_id,
637
+ secret_key=secret_key,
638
+ project_id=project_id,
639
+ )
640
+ config.session = self.session
641
+ if not self.session:
642
+ self.logger.warning("[ALERT] No session in config OR manual, skipping alert manager initialization")
643
+ self._alert_manager_initialized = True
644
+ return
645
+
646
+ rpc = config.session.rpc
647
+
648
+ # Determine environment: Localhost vs Cloud
649
+ # We use LPR server info to determine if we are local or cloud, similar to face_recognition_client
650
+ is_localhost = False
651
+ lpr_server_id = config.lpr_server_id
652
+ print("--------------------------------CONFIG-PRINT---------------------------")
653
+ print(config)
654
+ print("--------------------------------CONFIG-PRINT---------------------------")
655
+ if lpr_server_id:
656
+ try:
657
+ # Fetch LPR server info to compare IPs
658
+ response = rpc.get(f"/v1/actions/lpr_servers/{lpr_server_id}")
659
+ if response.get("success", False) and response.get("data"):
660
+ server_data = response.get("data", {})
661
+ server_host = server_data.get("host", "")
662
+ public_ip = self._get_public_ip()
663
+
664
+ # Check if server_host indicates localhost
665
+ localhost_indicators = ["localhost", "127.0.0.1", "0.0.0.0"]
666
+ if server_host in localhost_indicators or server_host == public_ip:
667
+ is_localhost = True
668
+ self.logger.info(f"[ALERT] Detected Localhost environment (Public IP={public_ip}, Server IP={server_host})")
669
+ else:
670
+ is_localhost = False
671
+ self.logger.info(f"[ALERT] Detected Cloud environment (Public IP={public_ip}, Server IP={server_host})")
672
+ else:
673
+ self.logger.warning(f"[ALERT] Failed to fetch LPR server info for environment detection, defaulting to Cloud mode")
674
+ except Exception as e:
675
+ self.logger.warning(f"[ALERT] Error detecting environment: {e}, defaulting to Cloud mode")
676
+ else:
677
+ self.logger.info("[ALERT] No LPR server ID, defaulting to Cloud mode")
678
+
679
+ # ------------------------------------------------------------------
680
+ # Discover action_id and fetch action details (STRICT API-DRIVEN)
681
+ # ------------------------------------------------------------------
682
+ action_id = self._discover_action_id()
683
+ if not action_id:
684
+ self.logger.error("[ALERT] Could not discover action_id from working directory or parents")
685
+ print("----- ALERT ACTION DISCOVERY -----")
686
+ print("action_id: NOT FOUND")
687
+ print("----------------------------------")
688
+ self._alert_manager_initialized = True
689
+ return
690
+
691
+ try:
692
+ action_url = f"/v1/actions/action/{action_id}/details"
693
+ action_resp = rpc.get(action_url)
694
+ if not (action_resp and action_resp.get("success", False)):
695
+ raise RuntimeError(action_resp.get("message", "Unknown error") if isinstance(action_resp, dict) else "Unknown error")
696
+ action_doc = action_resp.get("data", {}) if isinstance(action_resp, dict) else {}
697
+ action_details = action_doc.get("actionDetails", {}) if isinstance(action_doc, dict) else {}
698
+
699
+ # server id and type extraction (robust to variants)
700
+ server_id = (
701
+ action_details.get("serverId")
702
+ or action_details.get("server_id")
703
+ or action_details.get("serverID")
704
+ or action_details.get("redis_server_id")
705
+ or action_details.get("kafka_server_id")
706
+ )
707
+ server_type = (
708
+ action_details.get("serverType")
709
+ or action_details.get("server_type")
710
+ or action_details.get("type")
711
+ )
712
+
713
+ # Persist identifiers for future
714
+ self._action_id = action_id
715
+ self._deployment_id = action_details.get("_idDeployment") or action_details.get("deployment_id")
716
+ self._app_deployment_id = action_details.get("app_deployment_id")
717
+ self._instance_id = action_details.get("instanceID") or action_details.get("instanceId")
718
+ self._external_ip = action_details.get("externalIP") or action_details.get("externalIp")
719
+
720
+ print("----- ALERT ACTION DETAILS -----")
721
+ print(f"action_id: {action_id}")
722
+ print(f"server_type: {server_type}")
723
+ print(f"server_id: {server_id}")
724
+ print(f"deployment_id: {self._deployment_id}")
725
+ print(f"app_deployment_id: {self._app_deployment_id}")
726
+ print(f"instance_id: {self._instance_id}")
727
+ print(f"external_ip: {self._external_ip}")
728
+ print("--------------------------------")
729
+ self.logger.info(f"[ALERT] Action details fetched | action_id={action_id}, server_type={server_type}, server_id={server_id}")
730
+ self.logger.debug(f"[ALERT] Full action_details: {action_details}")
731
+ except Exception as e:
732
+ self.logger.error(f"[ALERT] Failed to fetch action details for action_id={action_id}: {e}", exc_info=True)
733
+ print("----- ALERT ACTION DETAILS ERROR -----")
734
+ print(f"action_id: {action_id}")
735
+ print(f"error: {e}")
736
+ print("--------------------------------------")
737
+ self._alert_manager_initialized = True
738
+ return
739
+
740
+ redis_client = None
741
+ kafka_client = None
742
+
743
+ # STRICT SWITCH: Only Redis if localhost, Only Kafka if cloud
744
+ if is_localhost:
745
+ # Initialize Redis client (ONLY) using STRICT API by instanceID
746
+ instance_id = getattr(self, "_instance_id", None)
747
+ if not instance_id:
748
+ self.logger.error("[ALERT] Localhost mode but instance_id missing in action details for Redis initialization")
749
+ else:
750
+ try:
751
+ backend_base = self._get_backend_base_url()
752
+ url = f"/v1/actions/get_redis_server_by_instance_id/{instance_id}"
753
+ self.logger.info(f"[ALERT] Initializing Redis client via API for Localhost mode (instance_id={instance_id})")
754
+ response = rpc.get(url)
755
+ if isinstance(response, dict) and response.get("success", False):
756
+ data = response.get("data", {})
757
+ host = data.get("host")
758
+ port = data.get("port")
759
+ username = data.get("username")
760
+ password = data.get("password", "")
761
+ db_index = data.get("db", 0)
762
+ conn_timeout = data.get("connection_timeout", 120)
763
+
764
+ print("----- REDIS SERVER PARAMS -----")
765
+ print(f"server_type: {server_type}")
766
+ print(f"instance_id: {instance_id}")
767
+ print(f"host: {host}")
768
+ print(f"port: {port}")
769
+ print(f"username: {username}")
770
+ print(f"password: {password}")
771
+ print(f"db: {db_index}")
772
+ print(f"connection_timeout: {conn_timeout}")
773
+ print("--------------------------------")
774
+
775
+ self.logger.info(f"[ALERT] Redis server params | instance_id={instance_id}, host={host}, port={port}, user={username}, db={db_index}")
776
+
777
+ # Initialize without gating on status
778
+ redis_client = MatriceStream(
779
+ StreamType.REDIS,
780
+ host=host,
781
+ port=int(port),
782
+ password=password,
783
+ username=username,
784
+ db=db_index,
785
+ connection_timeout=conn_timeout
786
+ )
787
+ redis_client.setup("alert_instant_config_request")
788
+ self.logger.info("[ALERT] Redis client initialized successfully")
789
+ else:
790
+ self.logger.warning(f"[ALERT] Failed to fetch Redis server info: {response.get('message', 'Unknown error') if isinstance(response, dict) else 'Unknown error'}")
791
+ except Exception as e:
792
+ self.logger.warning(f"[ALERT] Redis initialization failed: {e}")
793
+
794
+ else:
795
+ # Initialize Kafka client (ONLY) using STRICT API (global info endpoint)
796
+ try:
797
+ backend_base = self._get_backend_base_url()
798
+ url = f"/v1/actions/get_kafka_info"
799
+ self.logger.info("[ALERT] Initializing Kafka client via API for Cloud mode")
800
+ response = rpc.get(url)
801
+ if isinstance(response, dict) and response.get("success", False):
802
+ data = response.get("data", {})
803
+ enc_ip = data.get("ip")
804
+ enc_port = data.get("port")
805
+ ip_addr = None
806
+ port = None
807
+ try:
808
+ ip_addr = base64.b64decode(str(enc_ip)).decode("utf-8")
809
+ except Exception:
810
+ ip_addr = enc_ip
811
+ try:
812
+ port = base64.b64decode(str(enc_port)).decode("utf-8")
813
+ except Exception:
814
+ port = enc_port
815
+
816
+ print("----- KAFKA SERVER PARAMS -----")
817
+ print(f"server_type: {server_type}")
818
+ print(f"ipAddress: {ip_addr}")
819
+ print(f"port: {port}")
820
+ print("--------------------------------")
821
+
822
+ self.logger.info(f"[ALERT] Kafka server params | ip={ip_addr}, port={port}")
823
+
824
+ bootstrap_servers = f"{ip_addr}:{port}"
825
+ kafka_client = MatriceStream(
826
+ StreamType.KAFKA,
827
+ bootstrap_servers=bootstrap_servers,
828
+ sasl_mechanism="SCRAM-SHA-256",
829
+ sasl_username="matrice-sdk-user",
830
+ sasl_password="matrice-sdk-password",
831
+ security_protocol="SASL_PLAINTEXT"
832
+ )
833
+ kafka_client.setup("alert_instant_config_request", consumer_group_id="py_analytics_lpr_alerts")
834
+ self.logger.info(f"[ALERT] Kafka client initialized successfully (servers={bootstrap_servers})")
835
+ else:
836
+ self.logger.warning(f"[ALERT] Failed to fetch Kafka server info: {response.get('message', 'Unknown error') if isinstance(response, dict) else 'Unknown error'}")
837
+ except Exception as e:
838
+ self.logger.warning(f"[ALERT] Kafka initialization failed: {e}")
839
+
840
+ # Create alert manager if client is available
841
+ if redis_client or kafka_client:
842
+ self.alert_manager = ALERT_INSTANCE(
843
+ redis_client=redis_client,
844
+ kafka_client=kafka_client,
845
+ config_topic="alert_instant_config_request",
846
+ trigger_topic="alert_instant_triggered",
847
+ polling_interval=10, # Poll every 10 seconds
848
+ logger=self.logger
849
+ )
850
+ self.alert_manager.start()
851
+ transport = "Redis" if redis_client else "Kafka"
852
+ self.logger.info(f"[ALERT] Alert manager initialized and started with {transport} (polling every 10s)")
853
+ else:
854
+ self.logger.warning(f"[ALERT] No {'Redis' if is_localhost else 'Kafka'} client available for {'Localhost' if is_localhost else 'Cloud'} mode, alerts disabled")
855
+
856
+ except Exception as e:
857
+ self.logger.error(f"[ALERT] Alert manager initialization failed: {e}", exc_info=True)
858
+ finally:
859
+ self._alert_manager_initialized = True # Mark as initialized (don't retry every frame)
515
860
 
516
861
  def reset_tracker(self) -> None:
517
862
  """Reset the advanced tracker instance."""
@@ -537,6 +882,149 @@ class LicensePlateMonitorUseCase(BaseProcessor):
537
882
  self.reset_tracker()
538
883
  self.reset_plate_tracking()
539
884
  self.logger.info("All plate tracking state reset")
885
+
886
+ def _send_instant_alerts(
887
+ self,
888
+ detections: List[Dict[str, Any]],
889
+ stream_info: Optional[Dict[str, Any]],
890
+ config: LicensePlateMonitorConfig
891
+ ) -> None:
892
+ """
893
+ Send detection events to the instant alert system.
894
+
895
+ This method processes detections and sends them to the alert manager
896
+ for evaluation against active alert configurations.
897
+
898
+ Args:
899
+ detections: List of detection dictionaries with plate_text
900
+ stream_info: Stream information containing camera_id and other metadata
901
+ config: License plate monitoring configuration
902
+ """
903
+ self.logger.info(f"[ALERT_DEBUG] ========== SEND INSTANT ALERTS ==========")
904
+
905
+ if not self.alert_manager:
906
+ self.logger.debug("[ALERT_DEBUG] Alert manager not configured, skipping instant alerts")
907
+ return
908
+
909
+ if not detections:
910
+ self.logger.debug("[ALERT_DEBUG] No detections to send to alert manager")
911
+ return
912
+
913
+ self.logger.info(f"[ALERT_DEBUG] Processing {len(detections)} detection(s) for alerts")
914
+
915
+ # Extract metadata directly from stream_info with empty string defaults
916
+ # No complex nested checks - if not found, pass empty string (no errors)
917
+ camera_id = ""
918
+ app_deployment_id = ""
919
+ application_id = ""
920
+ camera_name = ""
921
+
922
+ if stream_info:
923
+ self.logger.debug(f"[ALERT_DEBUG] stream_info keys: {list(stream_info.keys())}")
924
+ # Direct extraction with safe defaults
925
+ camera_id = stream_info.get("camera_id", "")
926
+ if not camera_id and "camera_info" in stream_info:
927
+ camera_id = stream_info.get("camera_info", {}).get("camera_id", "")
928
+
929
+ camera_name = stream_info.get("camera_name", "")
930
+ if not camera_name and "camera_info" in stream_info:
931
+ camera_name = stream_info.get("camera_info", {}).get("camera_name", "")
932
+
933
+ app_deployment_id = stream_info.get("app_deployment_id", "")
934
+ application_id = stream_info.get("application_id", stream_info.get("app_id", ""))
935
+
936
+ self.logger.debug(f"[ALERT_DEBUG] Extracted metadata from stream_info:")
937
+ self.logger.debug(f"[ALERT_DEBUG] - camera_id: '{camera_id}'")
938
+ self.logger.debug(f"[ALERT_DEBUG] - camera_name: '{camera_name}'")
939
+ self.logger.debug(f"[ALERT_DEBUG] - app_deployment_id: '{app_deployment_id}'")
940
+ self.logger.debug(f"[ALERT_DEBUG] - application_id: '{application_id}'")
941
+ else:
942
+ self.logger.warning("[ALERT_DEBUG] stream_info is None")
943
+
944
+ # Process each detection with a valid plate_text
945
+ sent_count = 0
946
+ skipped_count = 0
947
+ for i, detection in enumerate(detections):
948
+ self.logger.debug(f"[ALERT_DEBUG] --- Processing detection #{i+1} ---")
949
+ self.logger.debug(f"[ALERT_DEBUG] Detection keys: {list(detection.keys())}")
950
+
951
+ plate_text = detection.get('plate_text', '.')
952
+ if plate_text:
953
+ plate_text = plate_text.strip()
954
+ else:
955
+ plate_text = ''
956
+ self.logger.debug(f"[ALERT_DEBUG] Plate text: '{plate_text}'")
957
+
958
+ if not plate_text or plate_text == '':
959
+ self.logger.debug(f"[ALERT_DEBUG] Skipping detection #{i+1} - no plate_text")
960
+ skipped_count += 1
961
+ continue
962
+
963
+ # Extract detection metadata
964
+ confidence = detection.get('score', detection.get('confidence', 0.0))
965
+ bbox = detection.get('bbox', detection.get('bounding_box', []))
966
+
967
+ self.logger.debug(f"[ALERT_DEBUG] Confidence: {confidence}")
968
+ self.logger.debug(f"[ALERT_DEBUG] BBox: {bbox}")
969
+
970
+ # Build coordinates dict
971
+ coordinates = {}
972
+ if isinstance(bbox, dict):
973
+ # Handle dict format bbox
974
+ if 'xmin' in bbox:
975
+ coordinates = {
976
+ "x": int(bbox.get('xmin', 0)),
977
+ "y": int(bbox.get('ymin', 0)),
978
+ "width": int(bbox.get('xmax', 0) - bbox.get('xmin', 0)),
979
+ "height": int(bbox.get('ymax', 0) - bbox.get('ymin', 0))
980
+ }
981
+ elif 'x' in bbox:
982
+ coordinates = {
983
+ "x": int(bbox.get('x', 0)),
984
+ "y": int(bbox.get('y', 0)),
985
+ "width": int(bbox.get('width', 0)),
986
+ "height": int(bbox.get('height', 0))
987
+ }
988
+ elif isinstance(bbox, list) and len(bbox) >= 4:
989
+ x1, y1, x2, y2 = bbox[:4]
990
+ coordinates = {
991
+ "x": int(x1),
992
+ "y": int(y1),
993
+ "width": int(x2 - x1),
994
+ "height": int(y2 - y1)
995
+ }
996
+
997
+ self.logger.debug(f"[ALERT_DEBUG] Coordinates: {coordinates}")
998
+
999
+ # Build detection event for alert system
1000
+ detection_event = {
1001
+ "camera_id": camera_id,
1002
+ "app_deployment_id": app_deployment_id,
1003
+ "application_id": application_id,
1004
+ "detectionType": "license_plate",
1005
+ "plateNumber": plate_text,
1006
+ "confidence": float(confidence),
1007
+ "frameUrl": "", # Will be filled by analytics publisher if needed
1008
+ "coordinates": coordinates,
1009
+ "cameraName": camera_name,
1010
+ "vehicleType": detection.get('vehicle_type', ''),
1011
+ "vehicleColor": detection.get('vehicle_color', ''),
1012
+ "timestamp": datetime.now(timezone.utc).isoformat()
1013
+ }
1014
+
1015
+ self.logger.info(f"[ALERT_DEBUG] Detection event #{i+1} built: {detection_event}")
1016
+
1017
+ # Send to alert manager for evaluation
1018
+ try:
1019
+ self.logger.info(f"[ALERT_DEBUG] Sending detection event #{i+1} to alert manager...")
1020
+ self.alert_manager.process_detection_event(detection_event)
1021
+ self.logger.info(f"[ALERT_DEBUG] ✓ Sent detection event to alert manager: plate={plate_text}, confidence={confidence:.2f}")
1022
+ sent_count += 1
1023
+ except Exception as e:
1024
+ self.logger.error(f"[ALERT_DEBUG] ❌ Error sending detection event to alert manager: {e}", exc_info=True)
1025
+
1026
+ self.logger.info(f"[ALERT_DEBUG] Summary: {sent_count} sent, {skipped_count} skipped")
1027
+ self.logger.info(f"[ALERT_DEBUG] ========== INSTANT ALERTS PROCESSED ==========")
540
1028
 
541
1029
  def _initialize_plate_logger(self, config: LicensePlateMonitorConfig) -> bool:
542
1030
  """Initialize the plate logger if lpr_server_id is provided. Returns True if successful."""
@@ -576,6 +1064,7 @@ class LicensePlateMonitorUseCase(BaseProcessor):
576
1064
  self.logger.info(f"[LP_LOGGING] Starting plate logging check - detections count: {len(detections)}")
577
1065
  self.logger.info(f"[LP_LOGGING] Logging enabled: {self._logging_enabled}, Plate logger exists: {self.plate_logger is not None}, Stream info exists: {stream_info is not None}")
578
1066
 
1067
+ #self._logging_enabled = False # ToDo: DISABLED FOR NOW, ENABLED FOR PRODUCTION
579
1068
  if not self._logging_enabled:
580
1069
  print("[LP_LOGGING] Plate logging is DISABLED")
581
1070
  self.logger.warning("[LP_LOGGING] Plate logging is DISABLED - logging_enabled flag is False")
@@ -586,10 +1075,10 @@ class LicensePlateMonitorUseCase(BaseProcessor):
586
1075
  self.logger.warning("[LP_LOGGING] Plate logging SKIPPED - plate_logger is not initialized (lpr_server_id may not be configured)")
587
1076
  return
588
1077
 
589
- if not stream_info:
590
- print("[LP_LOGGING] Plate logging SKIPPED - stream_info is None")
591
- self.logger.warning("[LP_LOGGING] Plate logging SKIPPED - stream_info is None")
592
- return
1078
+ # if not stream_info:
1079
+ # print("[LP_LOGGING] Plate logging SKIPPED - stream_info is None")
1080
+ # self.logger.warning("[LP_LOGGING] Plate logging SKIPPED - stream_info is None")
1081
+ # return
593
1082
 
594
1083
  print("[LP_LOGGING] All pre-conditions met, proceeding with plate logging")
595
1084
  self.logger.info(f"[LP_LOGGING] All pre-conditions met, proceeding with plate logging")
@@ -617,6 +1106,7 @@ class LicensePlateMonitorUseCase(BaseProcessor):
617
1106
  else:
618
1107
  self.logger.warning(f"[LP_LOGGING] Failed to decode image bytes")
619
1108
  except Exception as e:
1109
+ #pass
620
1110
  self.logger.error(f"[LP_LOGGING] Exception while encoding frame image: {e}", exc_info=True)
621
1111
  else:
622
1112
  self.logger.info(f"[LP_LOGGING] No image_bytes provided, sending without image")
@@ -657,14 +1147,18 @@ class LicensePlateMonitorUseCase(BaseProcessor):
657
1147
  print(f"[LP_LOGGING] Plate {plate_text}: {status}")
658
1148
  self.logger.info(f"[LP_LOGGING] Plate {plate_text}: {status}")
659
1149
  except Exception as e:
1150
+ #pass
660
1151
  print(f"[LP_LOGGING] ERROR - Plate {plate_text} failed: {e}")
661
1152
  self.logger.error(f"[LP_LOGGING] Plate {plate_text} raised exception: {e}", exc_info=True)
662
1153
 
663
1154
  print("[LP_LOGGING] Plate logging complete")
664
1155
  self.logger.info(f"[LP_LOGGING] Plate logging complete")
665
1156
  except Exception as e:
1157
+ print(f"[LP_LOGGING] CRITICAL ERROR during plate logging: {e}")
1158
+
666
1159
  print(f"[LP_LOGGING] CRITICAL ERROR during plate logging: {e}")
667
1160
  self.logger.error(f"[LP_LOGGING] CRITICAL ERROR during plate logging: {e}", exc_info=True)
1161
+ pass
668
1162
  else:
669
1163
  print("[LP_LOGGING] No plates to log")
670
1164
  self.logger.info(f"[LP_LOGGING] No plates to log (plates_to_log is empty)")
@@ -693,11 +1187,16 @@ class LicensePlateMonitorUseCase(BaseProcessor):
693
1187
  else:
694
1188
  self.logger.error(f"[LP_LOGGING] Plate logger initialization FAILED - plates will NOT be sent")
695
1189
  elif self._plate_logger_initialized:
696
- self.logger.debug(f"[LP_LOGGING] Plate logger already initialized, skipping re-initialization")
1190
+ self.logger.debug(f"[LP_LOGGING] Plate logger already initialized, skipping re-initialization")
697
1191
  elif not config.lpr_server_id:
698
- if self._total_frame_counter == 0: # Only log once at start
699
- self.logger.warning(f"[LP_LOGGING] Plate logging will be DISABLED - no lpr_server_id provided in config")
700
-
1192
+ if self._total_frame_counter == 0: #Only log once at start
1193
+ self.logger.warning(f"[LP_LOGGING] Plate logging will be DISABLED - no lpr_server_id provided in config")
1194
+
1195
+ # Initialize alert manager once (lazy initialization on first call)
1196
+ if not self._alert_manager_initialized:
1197
+ self._initialize_alert_manager_once(config)
1198
+ self.logger.info(f"[ALERT] CONFIG OF ALERT SHOULD BE PRINTED")
1199
+
701
1200
  # Normalize alert_config if provided as a plain dict (JS JSON)
702
1201
  if isinstance(getattr(config, 'alert_config', None), dict):
703
1202
  try:
@@ -718,13 +1217,13 @@ class LicensePlateMonitorUseCase(BaseProcessor):
718
1217
  # print("---------CONFIDENCE FILTERING",config.confidence_threshold)
719
1218
  # print("---------DATA1--------------",data)
720
1219
  processed_data = filter_by_confidence(data, config.confidence_threshold)
721
- #self.logger.debug(f"Applied confidence filtering with threshold {config.confidence_threshold}")
1220
+ self.logger.debug(f"Applied confidence filtering with threshold {config.confidence_threshold}")
722
1221
 
723
1222
  # Step 2: Apply category mapping if provided
724
1223
  if config.index_to_category:
725
1224
  processed_data = apply_category_mapping(processed_data, config.index_to_category)
726
1225
  #self.logger.debug("Applied category mapping")
727
- #print("---------DATA2--------------",processed_data)
1226
+ print("---------DATA2-STREAM--------------",stream_info)
728
1227
  # Step 3: Filter to target categories (handle dict or list)
729
1228
  if isinstance(processed_data, dict):
730
1229
  processed_data = processed_data.get("detections", [])
@@ -775,25 +1274,29 @@ class LicensePlateMonitorUseCase(BaseProcessor):
775
1274
  #print("---------DATA5--------------",processed_data)
776
1275
  # Step 8: Perform OCR on media
777
1276
  ocr_analysis = self._analyze_ocr_in_media(processed_data, input_bytes, config)
778
- self.logger.info(f"[LP_LOGGING] OCR analysis completed, found {len(ocr_analysis)} results")
1277
+ #self.logger.info(f"[LP_LOGGING] OCR analysis completed, found {len(ocr_analysis)} results")
779
1278
  ocr_plates_found = [r.get('plate_text') for r in ocr_analysis if r.get('plate_text')]
780
- if ocr_plates_found:
781
- self.logger.info(f"[LP_LOGGING] OCR detected plates: {ocr_plates_found}")
782
- else:
783
- self.logger.warning(f"[LP_LOGGING] OCR did not detect any valid plate texts")
1279
+ # if ocr_plates_found:
1280
+ # self.logger.info(f"[LP_LOGGING] OCR detected plates: {ocr_plates_found}")
1281
+ # else:
1282
+ # self.logger.warning(f"[LP_LOGGING] OCR did not detect any valid plate texts")
784
1283
 
785
1284
  # Step 9: Update plate texts
786
1285
  processed_data = self._update_detections_with_ocr(processed_data, ocr_analysis)
787
1286
  self._update_plate_texts(processed_data)
788
-
1287
+ print("[LP_LOGGING]DEBUG -1")
1288
+
789
1289
  # Log final detection state before sending
790
1290
  final_plates = [d.get('plate_text') for d in processed_data if d.get('plate_text')]
791
1291
  self.logger.info(f"[LP_LOGGING] After OCR update, {len(final_plates)} detections have plate_text: {final_plates}")
792
-
1292
+
793
1293
  # Step 9.5: Log detected plates to RPC (optional, only if lpr_server_id is provided)
794
1294
  # Direct await since process is now async
795
1295
  await self._log_detected_plates(processed_data, config, stream_info, input_bytes)
796
-
1296
+ print("[LP_LOGGING]DEBUG -2")
1297
+ # Step 9.6: Send detections to instant alert system (if configured)
1298
+ self._send_instant_alerts(processed_data, stream_info, config)
1299
+ print("[LP_LOGGING]DEBUG -3")
797
1300
  # Step 10: Update frame counter
798
1301
  self._total_frame_counter += 1
799
1302
 
@@ -809,6 +1312,7 @@ class LicensePlateMonitorUseCase(BaseProcessor):
809
1312
  # Step 12: Calculate summaries
810
1313
  counting_summary = self._count_categories(processed_data, config)
811
1314
  counting_summary['total_counts'] = self.get_total_counts()
1315
+ print("[LP_LOGGING]DEBUG -4")
812
1316
 
813
1317
  # Step 13: Generate alerts and summaries
814
1318
  alerts = self._check_alerts(counting_summary, frame_number, config)
@@ -856,7 +1360,7 @@ class LicensePlateMonitorUseCase(BaseProcessor):
856
1360
  processing_fps = (1.0 / proc_time) if proc_time > 0 else None
857
1361
  # Log the performance metrics using the module-level logger
858
1362
  print("latency in ms:",processing_latency_ms,"| Throughput fps:",processing_fps,"| Frame_Number:",self._total_frame_counter)
859
-
1363
+ print("[LP_LOGGING]DEBUG -5")
860
1364
  return result
861
1365
 
862
1366
  except Exception as e:
@@ -1162,35 +1666,39 @@ class LicensePlateMonitorUseCase(BaseProcessor):
1162
1666
  human_text_lines = []
1163
1667
  #print("counting_summary", counting_summary)
1164
1668
  human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}:")
1669
+ sum_of_current_frame_detections = sum(per_category_count.values())
1670
+
1165
1671
  if total_detections > 0:
1672
+ #for cat, count in per_category_count.items():
1673
+ human_text_lines.append(f"\t- License Plates Detected: {sum_of_current_frame_detections}")
1166
1674
  category_counts = [f"{count} {cat}" for cat, count in per_category_count.items()]
1167
1675
  detection_text = category_counts[0] + " detected" if len(category_counts) == 1 else f"{', '.join(category_counts[:-1])}, and {category_counts[-1]} detected"
1168
- human_text_lines.append(f"\t- {detection_text}")
1169
- # Show dominant per-track license plates for current frame
1676
+ #human_text_lines.append(f"\t- {detection_text}")
1677
+ #Show dominant per-track license plates for current frame
1170
1678
  seen = set()
1171
1679
  display_texts = []
1172
1680
  for det in counting_summary.get("detections", []):
1173
1681
  t = det.get("track_id")
1174
1682
  dom = det.get("plate_text")
1175
- if not dom or not (self._min_plate_len <= len(dom) <= 6):
1683
+ if not dom or not (self._min_plate_len <= len(dom) <= 5):
1176
1684
  continue
1177
1685
  if t in seen:
1178
1686
  continue
1179
1687
  seen.add(t)
1180
1688
  display_texts.append(dom)
1181
- if display_texts:
1182
- human_text_lines.append(f"\t- License Plates: {', '.join(display_texts)}")
1689
+ # if display_texts:
1690
+ # human_text_lines.append(f"\t- License Plates: {', '.join(display_texts)}")
1183
1691
  else:
1184
- human_text_lines.append(f"\t- No detections")
1692
+ human_text_lines.append(f"\t- License Plates Detected: 0")
1185
1693
 
1186
1694
  human_text_lines.append("")
1187
- human_text_lines.append(f"TOTAL SINCE {start_timestamp}:")
1188
- human_text_lines.append(f"\t- Total Detected: {cumulative_total}")
1695
+ # human_text_lines.append(f"TOTAL SINCE {start_timestamp}:")
1696
+ # human_text_lines.append(f"\t- Total Detected: {cumulative_total}")
1189
1697
 
1190
- if self._unique_plate_texts:
1191
- human_text_lines.append("\t- Unique License Plates:")
1192
- for text in sorted(self._unique_plate_texts.values()):
1193
- human_text_lines.append(f"\t\t- {text}")
1698
+ # if self._unique_plate_texts:
1699
+ # human_text_lines.append("\t- Unique License Plates:")
1700
+ # for text in sorted(self._unique_plate_texts.values()):
1701
+ # human_text_lines.append(f"\t\t- {text}")
1194
1702
 
1195
1703
  current_counts = [{"category": cat, "count": count} for cat, count in per_category_count.items() if count > 0 or total_detections > 0]
1196
1704
  total_counts_list = [{"category": cat, "count": count} for cat, count in total_counts.items() if count > 0 or cumulative_total > 0]
@@ -1200,10 +1708,10 @@ class LicensePlateMonitorUseCase(BaseProcessor):
1200
1708
  for detection in counting_summary.get("detections", []):
1201
1709
  dom = detection.get("plate_text", "")
1202
1710
  if not dom:
1203
- dom = "license_plate"
1711
+ dom = ""
1204
1712
  bbox = detection.get("bounding_box", {})
1205
- category = detection.get("category", "license_plate")
1206
- segmentation = detection.get("masks", detection.get("segmentation", detection.get("mask", [])))
1713
+ category = detection.get("category", "")
1714
+ #egmentation = detection.get("masks", detection.get("segmentation", detection.get("mask", [])))
1207
1715
  detection_obj = self.create_detection_object(category, bbox, segmentation=None, plate_text=dom)
1208
1716
  detections.append(detection_obj)
1209
1717