matrice-analytics 0.1.60__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 (196) hide show
  1. matrice_analytics/__init__.py +28 -0
  2. matrice_analytics/boundary_drawing_internal/README.md +305 -0
  3. matrice_analytics/boundary_drawing_internal/__init__.py +45 -0
  4. matrice_analytics/boundary_drawing_internal/boundary_drawing_internal.py +1207 -0
  5. matrice_analytics/boundary_drawing_internal/boundary_drawing_tool.py +429 -0
  6. matrice_analytics/boundary_drawing_internal/boundary_tool_template.html +1036 -0
  7. matrice_analytics/boundary_drawing_internal/data/.gitignore +12 -0
  8. matrice_analytics/boundary_drawing_internal/example_usage.py +206 -0
  9. matrice_analytics/boundary_drawing_internal/usage/README.md +110 -0
  10. matrice_analytics/boundary_drawing_internal/usage/boundary_drawer_launcher.py +102 -0
  11. matrice_analytics/boundary_drawing_internal/usage/simple_boundary_launcher.py +107 -0
  12. matrice_analytics/post_processing/README.md +455 -0
  13. matrice_analytics/post_processing/__init__.py +732 -0
  14. matrice_analytics/post_processing/advanced_tracker/README.md +650 -0
  15. matrice_analytics/post_processing/advanced_tracker/__init__.py +17 -0
  16. matrice_analytics/post_processing/advanced_tracker/base.py +99 -0
  17. matrice_analytics/post_processing/advanced_tracker/config.py +77 -0
  18. matrice_analytics/post_processing/advanced_tracker/kalman_filter.py +370 -0
  19. matrice_analytics/post_processing/advanced_tracker/matching.py +195 -0
  20. matrice_analytics/post_processing/advanced_tracker/strack.py +230 -0
  21. matrice_analytics/post_processing/advanced_tracker/tracker.py +367 -0
  22. matrice_analytics/post_processing/config.py +146 -0
  23. matrice_analytics/post_processing/core/__init__.py +63 -0
  24. matrice_analytics/post_processing/core/base.py +704 -0
  25. matrice_analytics/post_processing/core/config.py +3291 -0
  26. matrice_analytics/post_processing/core/config_utils.py +925 -0
  27. matrice_analytics/post_processing/face_reg/__init__.py +43 -0
  28. matrice_analytics/post_processing/face_reg/compare_similarity.py +556 -0
  29. matrice_analytics/post_processing/face_reg/embedding_manager.py +950 -0
  30. matrice_analytics/post_processing/face_reg/face_recognition.py +2234 -0
  31. matrice_analytics/post_processing/face_reg/face_recognition_client.py +606 -0
  32. matrice_analytics/post_processing/face_reg/people_activity_logging.py +321 -0
  33. matrice_analytics/post_processing/ocr/__init__.py +0 -0
  34. matrice_analytics/post_processing/ocr/easyocr_extractor.py +250 -0
  35. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/__init__.py +9 -0
  36. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/__init__.py +4 -0
  37. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/cli.py +33 -0
  38. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/dataset_stats.py +139 -0
  39. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/export.py +398 -0
  40. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/train.py +447 -0
  41. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/utils.py +129 -0
  42. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/valid.py +93 -0
  43. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/validate_dataset.py +240 -0
  44. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_augmentation.py +176 -0
  45. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_predictions.py +96 -0
  46. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/__init__.py +3 -0
  47. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/process.py +246 -0
  48. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/types.py +60 -0
  49. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/utils.py +87 -0
  50. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/__init__.py +3 -0
  51. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/config.py +82 -0
  52. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/hub.py +141 -0
  53. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/plate_recognizer.py +323 -0
  54. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/py.typed +0 -0
  55. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/__init__.py +0 -0
  56. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/__init__.py +0 -0
  57. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/augmentation.py +101 -0
  58. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/dataset.py +97 -0
  59. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/__init__.py +0 -0
  60. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/config.py +114 -0
  61. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/layers.py +553 -0
  62. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/loss.py +55 -0
  63. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/metric.py +86 -0
  64. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_builders.py +95 -0
  65. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_schema.py +395 -0
  66. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/__init__.py +0 -0
  67. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/backend_utils.py +38 -0
  68. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/utils.py +214 -0
  69. matrice_analytics/post_processing/ocr/postprocessing.py +270 -0
  70. matrice_analytics/post_processing/ocr/preprocessing.py +52 -0
  71. matrice_analytics/post_processing/post_processor.py +1175 -0
  72. matrice_analytics/post_processing/test_cases/__init__.py +1 -0
  73. matrice_analytics/post_processing/test_cases/run_tests.py +143 -0
  74. matrice_analytics/post_processing/test_cases/test_advanced_customer_service.py +841 -0
  75. matrice_analytics/post_processing/test_cases/test_basic_counting_tracking.py +523 -0
  76. matrice_analytics/post_processing/test_cases/test_comprehensive.py +531 -0
  77. matrice_analytics/post_processing/test_cases/test_config.py +852 -0
  78. matrice_analytics/post_processing/test_cases/test_customer_service.py +585 -0
  79. matrice_analytics/post_processing/test_cases/test_data_generators.py +583 -0
  80. matrice_analytics/post_processing/test_cases/test_people_counting.py +510 -0
  81. matrice_analytics/post_processing/test_cases/test_processor.py +524 -0
  82. matrice_analytics/post_processing/test_cases/test_usecases.py +165 -0
  83. matrice_analytics/post_processing/test_cases/test_utilities.py +356 -0
  84. matrice_analytics/post_processing/test_cases/test_utils.py +743 -0
  85. matrice_analytics/post_processing/usecases/Histopathological_Cancer_Detection_img.py +604 -0
  86. matrice_analytics/post_processing/usecases/__init__.py +267 -0
  87. matrice_analytics/post_processing/usecases/abandoned_object_detection.py +797 -0
  88. matrice_analytics/post_processing/usecases/advanced_customer_service.py +1601 -0
  89. matrice_analytics/post_processing/usecases/age_detection.py +842 -0
  90. matrice_analytics/post_processing/usecases/age_gender_detection.py +1085 -0
  91. matrice_analytics/post_processing/usecases/anti_spoofing_detection.py +656 -0
  92. matrice_analytics/post_processing/usecases/assembly_line_detection.py +841 -0
  93. matrice_analytics/post_processing/usecases/banana_defect_detection.py +624 -0
  94. matrice_analytics/post_processing/usecases/basic_counting_tracking.py +667 -0
  95. matrice_analytics/post_processing/usecases/blood_cancer_detection_img.py +881 -0
  96. matrice_analytics/post_processing/usecases/car_damage_detection.py +834 -0
  97. matrice_analytics/post_processing/usecases/car_part_segmentation.py +946 -0
  98. matrice_analytics/post_processing/usecases/car_service.py +1601 -0
  99. matrice_analytics/post_processing/usecases/cardiomegaly_classification.py +864 -0
  100. matrice_analytics/post_processing/usecases/cell_microscopy_segmentation.py +897 -0
  101. matrice_analytics/post_processing/usecases/chicken_pose_detection.py +648 -0
  102. matrice_analytics/post_processing/usecases/child_monitoring.py +814 -0
  103. matrice_analytics/post_processing/usecases/color/clip.py +660 -0
  104. matrice_analytics/post_processing/usecases/color/clip_processor/merges.txt +48895 -0
  105. matrice_analytics/post_processing/usecases/color/clip_processor/preprocessor_config.json +28 -0
  106. matrice_analytics/post_processing/usecases/color/clip_processor/special_tokens_map.json +30 -0
  107. matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer.json +245079 -0
  108. matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer_config.json +32 -0
  109. matrice_analytics/post_processing/usecases/color/clip_processor/vocab.json +1 -0
  110. matrice_analytics/post_processing/usecases/color/color_map_utils.py +70 -0
  111. matrice_analytics/post_processing/usecases/color/color_mapper.py +468 -0
  112. matrice_analytics/post_processing/usecases/color_detection.py +1936 -0
  113. matrice_analytics/post_processing/usecases/color_map_utils.py +70 -0
  114. matrice_analytics/post_processing/usecases/concrete_crack_detection.py +827 -0
  115. matrice_analytics/post_processing/usecases/crop_weed_detection.py +781 -0
  116. matrice_analytics/post_processing/usecases/customer_service.py +1008 -0
  117. matrice_analytics/post_processing/usecases/defect_detection_products.py +936 -0
  118. matrice_analytics/post_processing/usecases/distracted_driver_detection.py +822 -0
  119. matrice_analytics/post_processing/usecases/drone_traffic_monitoring.py +585 -0
  120. matrice_analytics/post_processing/usecases/drowsy_driver_detection.py +829 -0
  121. matrice_analytics/post_processing/usecases/dwell_detection.py +829 -0
  122. matrice_analytics/post_processing/usecases/emergency_vehicle_detection.py +827 -0
  123. matrice_analytics/post_processing/usecases/face_emotion.py +813 -0
  124. matrice_analytics/post_processing/usecases/face_recognition.py +827 -0
  125. matrice_analytics/post_processing/usecases/fashion_detection.py +835 -0
  126. matrice_analytics/post_processing/usecases/field_mapping.py +902 -0
  127. matrice_analytics/post_processing/usecases/fire_detection.py +1146 -0
  128. matrice_analytics/post_processing/usecases/flare_analysis.py +836 -0
  129. matrice_analytics/post_processing/usecases/flower_segmentation.py +1006 -0
  130. matrice_analytics/post_processing/usecases/gas_leak_detection.py +837 -0
  131. matrice_analytics/post_processing/usecases/gender_detection.py +832 -0
  132. matrice_analytics/post_processing/usecases/human_activity_recognition.py +871 -0
  133. matrice_analytics/post_processing/usecases/intrusion_detection.py +1672 -0
  134. matrice_analytics/post_processing/usecases/leaf.py +821 -0
  135. matrice_analytics/post_processing/usecases/leaf_disease.py +840 -0
  136. matrice_analytics/post_processing/usecases/leak_detection.py +837 -0
  137. matrice_analytics/post_processing/usecases/license_plate_detection.py +1188 -0
  138. matrice_analytics/post_processing/usecases/license_plate_monitoring.py +1781 -0
  139. matrice_analytics/post_processing/usecases/litter_monitoring.py +717 -0
  140. matrice_analytics/post_processing/usecases/mask_detection.py +869 -0
  141. matrice_analytics/post_processing/usecases/natural_disaster.py +907 -0
  142. matrice_analytics/post_processing/usecases/parking.py +787 -0
  143. matrice_analytics/post_processing/usecases/parking_space_detection.py +822 -0
  144. matrice_analytics/post_processing/usecases/pcb_defect_detection.py +888 -0
  145. matrice_analytics/post_processing/usecases/pedestrian_detection.py +808 -0
  146. matrice_analytics/post_processing/usecases/people_counting.py +706 -0
  147. matrice_analytics/post_processing/usecases/people_counting_bckp.py +1683 -0
  148. matrice_analytics/post_processing/usecases/people_tracking.py +1842 -0
  149. matrice_analytics/post_processing/usecases/pipeline_detection.py +605 -0
  150. matrice_analytics/post_processing/usecases/plaque_segmentation_img.py +874 -0
  151. matrice_analytics/post_processing/usecases/pothole_segmentation.py +915 -0
  152. matrice_analytics/post_processing/usecases/ppe_compliance.py +645 -0
  153. matrice_analytics/post_processing/usecases/price_tag_detection.py +822 -0
  154. matrice_analytics/post_processing/usecases/proximity_detection.py +1901 -0
  155. matrice_analytics/post_processing/usecases/road_lane_detection.py +623 -0
  156. matrice_analytics/post_processing/usecases/road_traffic_density.py +832 -0
  157. matrice_analytics/post_processing/usecases/road_view_segmentation.py +915 -0
  158. matrice_analytics/post_processing/usecases/shelf_inventory_detection.py +583 -0
  159. matrice_analytics/post_processing/usecases/shoplifting_detection.py +822 -0
  160. matrice_analytics/post_processing/usecases/shopping_cart_analysis.py +899 -0
  161. matrice_analytics/post_processing/usecases/skin_cancer_classification_img.py +864 -0
  162. matrice_analytics/post_processing/usecases/smoker_detection.py +833 -0
  163. matrice_analytics/post_processing/usecases/solar_panel.py +810 -0
  164. matrice_analytics/post_processing/usecases/suspicious_activity_detection.py +1030 -0
  165. matrice_analytics/post_processing/usecases/template_usecase.py +380 -0
  166. matrice_analytics/post_processing/usecases/theft_detection.py +648 -0
  167. matrice_analytics/post_processing/usecases/traffic_sign_monitoring.py +724 -0
  168. matrice_analytics/post_processing/usecases/underground_pipeline_defect_detection.py +775 -0
  169. matrice_analytics/post_processing/usecases/underwater_pollution_detection.py +842 -0
  170. matrice_analytics/post_processing/usecases/vehicle_monitoring.py +1029 -0
  171. matrice_analytics/post_processing/usecases/warehouse_object_segmentation.py +899 -0
  172. matrice_analytics/post_processing/usecases/waterbody_segmentation.py +923 -0
  173. matrice_analytics/post_processing/usecases/weapon_detection.py +771 -0
  174. matrice_analytics/post_processing/usecases/weld_defect_detection.py +615 -0
  175. matrice_analytics/post_processing/usecases/wildlife_monitoring.py +898 -0
  176. matrice_analytics/post_processing/usecases/windmill_maintenance.py +834 -0
  177. matrice_analytics/post_processing/usecases/wound_segmentation.py +856 -0
  178. matrice_analytics/post_processing/utils/__init__.py +150 -0
  179. matrice_analytics/post_processing/utils/advanced_counting_utils.py +400 -0
  180. matrice_analytics/post_processing/utils/advanced_helper_utils.py +317 -0
  181. matrice_analytics/post_processing/utils/advanced_tracking_utils.py +461 -0
  182. matrice_analytics/post_processing/utils/alerting_utils.py +213 -0
  183. matrice_analytics/post_processing/utils/category_mapping_utils.py +94 -0
  184. matrice_analytics/post_processing/utils/color_utils.py +592 -0
  185. matrice_analytics/post_processing/utils/counting_utils.py +182 -0
  186. matrice_analytics/post_processing/utils/filter_utils.py +261 -0
  187. matrice_analytics/post_processing/utils/format_utils.py +293 -0
  188. matrice_analytics/post_processing/utils/geometry_utils.py +300 -0
  189. matrice_analytics/post_processing/utils/smoothing_utils.py +358 -0
  190. matrice_analytics/post_processing/utils/tracking_utils.py +234 -0
  191. matrice_analytics/py.typed +0 -0
  192. matrice_analytics-0.1.60.dist-info/METADATA +481 -0
  193. matrice_analytics-0.1.60.dist-info/RECORD +196 -0
  194. matrice_analytics-0.1.60.dist-info/WHEEL +5 -0
  195. matrice_analytics-0.1.60.dist-info/licenses/LICENSE.txt +21 -0
  196. matrice_analytics-0.1.60.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2234 @@
1
+ """
2
+ Face Recognition with Local Embedding Search Optimization
3
+
4
+ This module uses local similarity search for face recognition to achieve high performance:
5
+
6
+ Performance Optimization Strategy:
7
+ 1. EmbeddingManager loads all staff embeddings from API at startup (~1 API call)
8
+ 2. Embeddings are cached in memory as a normalized numpy matrix
9
+ 3. Face recognition uses fast local cosine similarity search (~1-5ms per face)
10
+ 4. API calls are avoided during normal operation (2000x speedup vs API calls)
11
+ 5. Background refresh updates embeddings periodically (configurable TTL)
12
+
13
+ Processing Flow:
14
+ 1. TemporalIdentityManager receives face embedding from detection
15
+ 2. Uses EmbeddingManager._find_best_local_match() for fast local similarity search
16
+ 3. Returns best match if similarity >= threshold, otherwise returns "Unknown"
17
+ 4. Only falls back to API if EmbeddingManager unavailable (rare)
18
+
19
+ Configuration options:
20
+ - enable_track_id_cache: Enable/disable track-level caching
21
+ - cache_max_size: Maximum number of cached track IDs (default: 3000)
22
+ - cache_ttl: Cache time-to-live in seconds (default: 3600)
23
+ - background_refresh_interval: Embedding refresh interval (default: 43200s = 12h)
24
+ - similarity_threshold: Minimum similarity for recognition (default: 0.45)
25
+ """
26
+ import subprocess
27
+ import logging
28
+ import asyncio
29
+ import os
30
+ log_file = open("pip_jetson_btii.log", "w")
31
+ cmd = ["pip", "install", "httpx"]
32
+ subprocess.run(
33
+ cmd,
34
+ stdout=log_file,
35
+ stderr=subprocess.STDOUT,
36
+ #preexec_fn=os.setpgrp
37
+ )
38
+ log_file.close()
39
+
40
+ from typing import Any, Dict, List, Optional, Tuple
41
+ import time
42
+ import base64
43
+ import cv2
44
+ import numpy as np
45
+ import threading
46
+ from datetime import datetime, timezone
47
+ from collections import deque
48
+
49
+ from ..core.base import (
50
+ BaseProcessor,
51
+ ProcessingContext,
52
+ ProcessingResult,
53
+ ConfigProtocol,
54
+ )
55
+ from ..utils import (
56
+ filter_by_confidence,
57
+ filter_by_categories,
58
+ apply_category_mapping,
59
+ calculate_counting_summary,
60
+ match_results_structure,
61
+ )
62
+ from dataclasses import dataclass, field
63
+ from ..core.config import BaseConfig, AlertConfig
64
+ from .face_recognition_client import FacialRecognitionClient
65
+ from .people_activity_logging import PeopleActivityLogging
66
+ from .embedding_manager import EmbeddingManager, EmbeddingConfig
67
+
68
+
69
+ # ---- Lightweight identity tracking and temporal smoothing (adapted from compare_similarity.py) ---- #
70
+ from collections import deque, defaultdict
71
+
72
+
73
+
74
+
75
+ def _normalize_embedding(vec: List[float]) -> List[float]:
76
+ """Normalize an embedding vector to unit length (L2). Returns float32 list."""
77
+ arr = np.asarray(vec, dtype=np.float32)
78
+ if arr.size == 0:
79
+ return []
80
+ n = np.linalg.norm(arr)
81
+ if n > 0:
82
+ arr = arr / n
83
+ return arr.tolist()
84
+
85
+
86
+ ## Removed FaceTracker fallback (using AdvancedTracker only)
87
+
88
+
89
+ class TemporalIdentityManager:
90
+ """
91
+ Maintains stable identity labels per tracker ID using temporal smoothing and embedding history.
92
+
93
+ Adaptation for production: _compute_best_identity uses EmbeddingManager for local similarity
94
+ search first (fast), then falls back to API only if needed (slow).
95
+ """
96
+
97
+ def __init__(
98
+ self,
99
+ face_client: FacialRecognitionClient,
100
+ embedding_manager = None,
101
+ recognition_threshold: float = 0.35,
102
+ history_size: int = 20,
103
+ unknown_patience: int = 7,
104
+ switch_patience: int = 5,
105
+ fallback_margin: float = 0.05,
106
+ ) -> None:
107
+ self.logger = logging.getLogger(__name__)
108
+ self.face_client = face_client
109
+ self.embedding_manager = embedding_manager
110
+ self.threshold = float(recognition_threshold)
111
+ self.history_size = int(history_size)
112
+ self.unknown_patience = int(unknown_patience)
113
+ self.switch_patience = int(switch_patience)
114
+ self.fallback_margin = float(fallback_margin)
115
+ self.tracks: Dict[Any, Dict[str, object]] = {}
116
+
117
+ def _ensure_track(self, track_id: Any) -> None:
118
+ if track_id not in self.tracks:
119
+ self.tracks[track_id] = {
120
+ "stable_staff_id": None,
121
+ "stable_person_name": None,
122
+ "stable_employee_id": None,
123
+ "stable_score": 0.0,
124
+ "stable_staff_details": {},
125
+ "label_votes": defaultdict(int), # staff_id -> votes
126
+ "embedding_history": deque(maxlen=self.history_size),
127
+ "unknown_streak": 0,
128
+ "streaks": defaultdict(int), # staff_id -> consecutive frames
129
+ }
130
+
131
+ async def _compute_best_identity(self, emb: List[float], location: str = "", timestamp: str = "") -> Tuple[Optional[str], str, float, Optional[str], Dict[str, Any], str]:
132
+ """
133
+ Find best identity match using local similarity search (fast) with optional API fallback.
134
+ Returns (staff_id, person_name, score, employee_id, staff_details, detection_type).
135
+
136
+ Performance optimization: Uses EmbeddingManager for local similarity search to avoid
137
+ slow API calls (~2000ms). Only falls back to API if local search is unavailable.
138
+ """
139
+ if not emb or not isinstance(emb, list):
140
+ return None, "Unknown", 0.0, None, {}, "unknown"
141
+
142
+ st10 = time.time()
143
+
144
+ # PRIMARY PATH: Local similarity search using EmbeddingManager (FAST - ~1-5ms)
145
+ if self.embedding_manager:
146
+ # Defensive check: ensure embeddings are loaded before attempting search
147
+ if not self.embedding_manager.is_ready():
148
+ status = self.embedding_manager.get_status()
149
+ self.logger.error(f"EmbeddingManager not ready for search - status: {status}")
150
+ print(f"ERROR: _compute_best_identity - embeddings not ready. Status: {status}")
151
+ return None, "Unknown", 0.0, None, {}, "unknown"
152
+
153
+ try:
154
+ local_match = self.embedding_manager._find_best_local_match(emb)
155
+
156
+ if local_match:
157
+ staff_embedding, similarity_score = local_match
158
+
159
+ # Extract person name from staff details
160
+ person_name = "Unknown"
161
+ staff_details = staff_embedding.staff_details if isinstance(staff_embedding.staff_details, dict) else {}
162
+
163
+ if staff_details:
164
+ first_name = staff_details.get("firstName")
165
+ last_name = staff_details.get("lastName")
166
+ name = staff_details.get("name")
167
+ if name:
168
+ person_name = str(name)
169
+ elif first_name or last_name:
170
+ person_name = f"{first_name or ''} {last_name or ''}".strip() or "Unknown"
171
+
172
+ # print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY (LOCAL)----------------------------")
173
+ # print("LATENCY:",(time.time() - st10)*1000,"| Throughput fps:",(1.0 / (time.time() - st10)) if (time.time() - st10) > 0 else None)
174
+ # print(f"LOCAL MATCH: staff_id={staff_embedding.staff_id}, similarity={similarity_score:.3f}")
175
+ # print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY (LOCAL)----------------------------")
176
+
177
+ self.logger.info(f"Local embedding match - staff_id={staff_embedding.staff_id}, person_name={person_name}, score={similarity_score:.3f}")
178
+
179
+ return (
180
+ str(staff_embedding.staff_id),
181
+ person_name,
182
+ float(similarity_score),
183
+ str(staff_embedding.employee_id),
184
+ staff_details,
185
+ "known"
186
+ )
187
+ else:
188
+ # No local match found; log best similarity for observability
189
+ best_sim = 0.0
190
+ try:
191
+ best_sim = float(self.embedding_manager.get_best_similarity(emb))
192
+ except Exception:
193
+ pass
194
+ self.logger.debug(f"No local match found - best_similarity={best_sim:.3f}, threshold={self.threshold:.3f}")
195
+ # print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY (LOCAL - NO MATCH)----------------------------")
196
+ # print("LATENCY:",(time.time() - st10)*1000,"| Throughput fps:",(1.0 / (time.time() - st10)) if (time.time() - st10) > 0 else None)
197
+ # print(f"BEST_SIM={best_sim:.3f} THRESH={self.threshold:.3f}")
198
+ # print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY (LOCAL - NO MATCH)----------------------------")
199
+
200
+ return None, "Unknown", 0.0, None, {}, "unknown"
201
+
202
+ except Exception as e:
203
+ self.logger.warning(f"Local similarity search failed, falling back to API: {e}")
204
+ # Fall through to API call below
205
+
206
+ # FALLBACK PATH: API call (SLOW - ~2000ms) - only if embedding manager not available
207
+ # This path should rarely be used in production
208
+ try:
209
+ self.logger.warning("Using slow API fallback for identity search - consider checking embedding manager initialization")
210
+ resp = await self.face_client.search_similar_faces(
211
+ face_embedding=emb,
212
+ threshold=0.01, # low threshold to always get top-1
213
+ limit=1,
214
+ collection="staff_enrollment",
215
+ location=location,
216
+ timestamp=timestamp,
217
+ )
218
+ # print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY (API FALLBACK)----------------------------")
219
+ # print("LATENCY:",(time.time() - st10)*1000,"| Throughput fps:",(1.0 / (time.time() - st10)) if (time.time() - st10) > 0 else None)
220
+ # print("WARNING: Using slow API fallback!")
221
+ # print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY (API FALLBACK)----------------------------")
222
+
223
+ except Exception as e:
224
+ self.logger.error(f"API ERROR: Failed to search similar faces in _compute_best_identity: {e}", exc_info=True)
225
+ return None, "Unknown", 0.0, None, {}, "unknown"
226
+
227
+ try:
228
+ results: List[Any] = []
229
+ self.logger.debug('API Response received for identity search')
230
+ if isinstance(resp, dict):
231
+ if isinstance(resp.get("data"), list):
232
+ results = resp.get("data", [])
233
+ elif isinstance(resp.get("results"), list):
234
+ results = resp.get("results", [])
235
+ elif isinstance(resp.get("items"), list):
236
+ results = resp.get("items", [])
237
+ elif isinstance(resp, list):
238
+ results = resp
239
+
240
+ if not results:
241
+ self.logger.debug("No identity match found from API")
242
+ return None, "Unknown", 0.0, None, {}, "unknown"
243
+
244
+ item = results[0] if isinstance(results, list) else results
245
+ self.logger.debug(f'Top-1 match from API: {item}')
246
+ # Be defensive with keys and types
247
+ staff_id = item.get("staffId") if isinstance(item, dict) else None
248
+ employee_id = str(item.get("_id")) if isinstance(item, dict) and item.get("_id") is not None else None
249
+ score = float(item.get("score", 0.0)) if isinstance(item, dict) else 0.0
250
+ detection_type = str(item.get("detectionType", "unknown")) if isinstance(item, dict) else "unknown"
251
+ staff_details = item.get("staffDetails", {}) if isinstance(item, dict) else {}
252
+ # Extract a person name from staff_details
253
+ person_name = "Unknown"
254
+ if isinstance(staff_details, dict) and staff_details:
255
+ first_name = staff_details.get("firstName")
256
+ last_name = staff_details.get("lastName")
257
+ name = staff_details.get("name")
258
+ if name:
259
+ person_name = str(name)
260
+ else:
261
+ if first_name or last_name:
262
+ person_name = f"{first_name or ''} {last_name or ''}".strip() or "UnknowNN" #TODO:ebugging change to normal once done
263
+ # If API says unknown or missing staff_id, treat as unknown
264
+ if not staff_id: #or detection_type == "unknown"
265
+ self.logger.debug(f"API returned unknown or missing staff_id - score={score}, employee_id={employee_id}")
266
+ return None, "Unknown", float(score), employee_id, staff_details if isinstance(staff_details, dict) else {}, "unknown"
267
+ self.logger.info(f"API identified face - staff_id={staff_id}, person_name={person_name}, score={score:.3f}")
268
+ return str(staff_id), person_name, float(score), employee_id, staff_details if isinstance(staff_details, dict) else {}, "known"
269
+ except Exception as e:
270
+ self.logger.error(f"Error parsing API response in _compute_best_identity: {e}", exc_info=True)
271
+ return None, "Unknown", 0.0, None, {}, "unknown"
272
+
273
+ async def _compute_best_identity_from_history(self, track_state: Dict[str, object], location: str = "", timestamp: str = "") -> Tuple[Optional[str], str, float, Optional[str], Dict[str, Any], str]:
274
+ hist: deque = track_state.get("embedding_history", deque()) # type: ignore
275
+ if not hist:
276
+ return None, "Unknown", 0.0, None, {}, "unknown"
277
+ try:
278
+ self.logger.debug(f"Computing identity from embedding history - history_size={len(hist)}")
279
+ proto = np.mean(np.asarray(list(hist), dtype=np.float32), axis=0)
280
+ proto_list = proto.tolist() if isinstance(proto, np.ndarray) else list(proto)
281
+ except Exception as e:
282
+ self.logger.error(f"Error computing prototype from history: {e}", exc_info=True)
283
+ proto_list = []
284
+ return await self._compute_best_identity(proto_list, location=location, timestamp=timestamp)
285
+
286
+ async def update(
287
+ self,
288
+ track_id: Any,
289
+ emb: List[float],
290
+ eligible_for_recognition: bool,
291
+ location: str = "",
292
+ timestamp: str = "",
293
+ ) -> Tuple[Optional[str], str, float, Optional[str], Dict[str, Any], str]:
294
+ """
295
+ Update temporal identity state for a track and return a stabilized identity.
296
+ Returns (staff_id, person_name, score, employee_id, staff_details, detection_type).
297
+ """
298
+ st7=time.time()
299
+ self._ensure_track(track_id)
300
+ s = self.tracks[track_id]
301
+
302
+ # Update embedding history
303
+ if emb:
304
+ try:
305
+ history: deque = s["embedding_history"] # type: ignore
306
+ history.append(_normalize_embedding(emb))
307
+ except Exception:
308
+ pass
309
+
310
+ # Defaults for return values
311
+ stable_staff_id = s.get("stable_staff_id")
312
+ stable_person_name = s.get("stable_person_name")
313
+ stable_employee_id = s.get("stable_employee_id")
314
+ stable_score = float(s.get("stable_score", 0.0))
315
+ stable_staff_details = s.get("stable_staff_details", {}) if isinstance(s.get("stable_staff_details"), dict) else {}
316
+
317
+ # print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - STABLE VALUES----------------------------")
318
+ # print("LATENCY:",(time.time() - st7)*1000,"| Throughput fps:",(1.0 / (time.time() - st7)) if (time.time() - st7) > 0 else None)
319
+ # print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - STABLE VALUES----------------------------")
320
+
321
+ if eligible_for_recognition and emb:
322
+ st8=time.time()
323
+ staff_id, person_name, inst_score, employee_id, staff_details, det_type = await self._compute_best_identity(
324
+ emb, location=location, timestamp=timestamp
325
+ )
326
+ # print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY_I----------------------------")
327
+ # print("LATENCY:",(time.time() - st8)*1000,"| Throughput fps:",(1.0 / (time.time() - st8)) if (time.time() - st8) > 0 else None)
328
+ # print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY_I----------------------------")
329
+
330
+ is_inst_known = staff_id is not None and inst_score >= self.threshold
331
+ if is_inst_known:
332
+ s["label_votes"][staff_id] += 1 # type: ignore
333
+ s["streaks"][staff_id] += 1 # type: ignore
334
+ s["unknown_streak"] = 0
335
+
336
+ # Initialize stable if not set
337
+ if stable_staff_id is None:
338
+ s["stable_staff_id"] = staff_id
339
+ s["stable_person_name"] = person_name
340
+ s["stable_employee_id"] = employee_id
341
+ s["stable_score"] = float(inst_score)
342
+ s["stable_staff_details"] = staff_details
343
+ return staff_id, person_name, float(inst_score), employee_id, staff_details, "known"
344
+
345
+ # If same as stable, keep it and update score
346
+ if staff_id == stable_staff_id:
347
+ s["stable_score"] = float(inst_score)
348
+ # prefer latest name/details if present
349
+ if person_name and person_name != stable_person_name:
350
+ s["stable_person_name"] = person_name
351
+ if isinstance(staff_details, dict) and staff_details:
352
+ s["stable_staff_details"] = staff_details
353
+ if employee_id:
354
+ s["stable_employee_id"] = employee_id
355
+ return staff_id, s.get("stable_person_name") or person_name, float(inst_score), s.get("stable_employee_id") or employee_id, s.get("stable_staff_details", {}), "known"
356
+
357
+ # Competing identity: switch only if sustained and with margin & votes ratio (local parity)
358
+ if s["streaks"][staff_id] >= self.switch_patience: # type: ignore
359
+ try:
360
+ prev_votes = s["label_votes"].get(stable_staff_id, 0) if stable_staff_id is not None else 0 # type: ignore
361
+ cand_votes = s["label_votes"].get(staff_id, 0) # type: ignore
362
+ except Exception:
363
+ prev_votes, cand_votes = 0, 0
364
+ if cand_votes >= max(2, 0.75 * prev_votes) and float(inst_score) >= (self.threshold + 0.02):
365
+ s["stable_staff_id"] = staff_id
366
+ s["stable_person_name"] = person_name
367
+ s["stable_employee_id"] = employee_id
368
+ s["stable_score"] = float(inst_score)
369
+ s["stable_staff_details"] = staff_details
370
+ # reset other streaks
371
+ try:
372
+ for k in list(s["streaks"].keys()): # type: ignore
373
+ if k != staff_id:
374
+ s["streaks"][k] = 0 # type: ignore
375
+ except Exception:
376
+ pass
377
+ return staff_id, person_name, float(inst_score), employee_id, staff_details, "known"
378
+
379
+ # Do not switch yet; keep stable but return instant score/name
380
+ return stable_staff_id, stable_person_name or person_name, float(inst_score), stable_employee_id or employee_id, stable_staff_details, "known" if stable_staff_id else "unknown"
381
+
382
+ # Instantaneous is unknown or low score
383
+ s["unknown_streak"] = int(s.get("unknown_streak", 0)) + 1
384
+ if stable_staff_id is not None and s["unknown_streak"] <= self.unknown_patience: # type: ignore
385
+ return stable_staff_id, stable_person_name or "Unknown", float(inst_score), stable_employee_id, stable_staff_details, "known"
386
+
387
+ # Fallback: use prototype from history
388
+ st9=time.time()
389
+ fb_staff_id, fb_name, fb_score, fb_employee_id, fb_details, fb_type = await self._compute_best_identity_from_history(s, location=location, timestamp=timestamp)
390
+ # print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY FROM HISTORY----------------------------")
391
+ # print("LATENCY:",(time.time() - st9)*1000,"| Throughput fps:",(1.0 / (time.time() - st9)) if (time.time() - st9) > 0 else None)
392
+ # print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY FROM HISTORY----------------------------")
393
+
394
+ if fb_staff_id is not None and fb_score >= max(0.0, self.threshold - self.fallback_margin):
395
+ s["label_votes"][fb_staff_id] += 1 # type: ignore
396
+ s["stable_staff_id"] = fb_staff_id
397
+ s["stable_person_name"] = fb_name
398
+ s["stable_employee_id"] = fb_employee_id
399
+ s["stable_score"] = float(fb_score)
400
+ s["stable_staff_details"] = fb_details
401
+ s["unknown_streak"] = 0
402
+ return fb_staff_id, fb_name, float(fb_score), fb_employee_id, fb_details, "known"
403
+
404
+ # No confident identity
405
+ s["stable_staff_id"] = stable_staff_id
406
+ s["stable_person_name"] = stable_person_name
407
+ s["stable_employee_id"] = stable_employee_id
408
+ s["stable_score"] = float(stable_score)
409
+ s["stable_staff_details"] = stable_staff_details
410
+ return None, "Unknown", float(inst_score), None, {}, "unknown"
411
+
412
+ # Not eligible or no embedding; keep stable if present
413
+ if stable_staff_id is not None:
414
+ return stable_staff_id, stable_person_name or "Unknown", float(stable_score), stable_employee_id, stable_staff_details, "known"
415
+ return None, "Unknown", 0.0, None, {}, "unknown"
416
+
417
+
418
+ @dataclass
419
+ class FaceRecognitionEmbeddingConfig(BaseConfig):
420
+ """Configuration for face recognition with embeddings use case."""
421
+
422
+ # Smoothing configuration
423
+ enable_smoothing: bool = False
424
+ smoothing_algorithm: str = "observability" # "window" or "observability"
425
+ smoothing_window_size: int = 20
426
+ smoothing_cooldown_frames: int = 5
427
+ smoothing_confidence_range_factor: float = 0.5
428
+
429
+ # Base confidence threshold (separate from embedding similarity threshold)
430
+ similarity_threshold: float = 0.45 #-- KEEP IT AT 0.45 ALWAYS
431
+ # Base confidence threshold (separate from embedding similarity threshold)
432
+ confidence_threshold: float = 0.1 #-- KEEP IT AT 0.1 ALWAYS
433
+
434
+ # Face recognition optional features
435
+ enable_face_tracking: bool = True # Enable BYTE TRACKER advanced face tracking -- KEEP IT TRUE ALWAYS
436
+
437
+
438
+ enable_auto_enrollment: bool = False # Enable auto-enrollment of unknown faces
439
+ enable_face_recognition: bool = (
440
+ True # Enable face recognition (requires credentials)
441
+ )
442
+ enable_unknown_face_processing: bool = (
443
+ False # TODO: Unable when we will be saving unkown faces # Enable unknown face cropping/uploading (requires frame data)
444
+ )
445
+ enable_people_activity_logging: bool = True # Enable logging of known face activities
446
+
447
+ usecase_categories: List[str] = field(default_factory=lambda: ["face"])
448
+
449
+ target_categories: List[str] = field(default_factory=lambda: ["face"])
450
+
451
+ alert_config: Optional[AlertConfig] = None
452
+
453
+ index_to_category: Optional[Dict[int, str]] = field(
454
+ default_factory=lambda: {0: "face"}
455
+ )
456
+
457
+ facial_recognition_server_id: str = ""
458
+ session: Any = None # Matrice session for face recognition client
459
+ deployment_id: Optional[str] = None # deployment ID for update_deployment call
460
+
461
+ # Embedding configuration
462
+ embedding_config: Optional[Any] = None # Will be set to EmbeddingConfig instance
463
+
464
+
465
+ # Track ID cache optimization settings
466
+ enable_track_id_cache: bool = True
467
+ cache_max_size: int = 3000
468
+ cache_ttl: int = 3600 # Cache time-to-live in seconds (1 hour)
469
+
470
+ # Search settings
471
+ search_limit: int = 5
472
+ search_collection: str = "staff_enrollment"
473
+
474
+
475
+ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
476
+ # Human-friendly display names for categories
477
+ CATEGORY_DISPLAY = {"face": "face"}
478
+
479
+ def __init__(self, config: Optional[FaceRecognitionEmbeddingConfig] = None):
480
+ super().__init__("face_recognition")
481
+ self.category = "security"
482
+
483
+ self.CASE_TYPE: Optional[str] = "face_recognition"
484
+ self.CASE_VERSION: Optional[str] = "1.0"
485
+ # List of categories to track
486
+ self.target_categories = ["face"]
487
+
488
+ # Initialize smoothing tracker
489
+ self.smoothing_tracker = None
490
+
491
+ # Initialize advanced tracker (will be created on first use)
492
+ self.tracker = None
493
+ # Initialize tracking state variables
494
+ self._total_frame_counter = 0
495
+ self._global_frame_offset = 0
496
+
497
+ # Track start time for "TOTAL SINCE" calculation
498
+ self._tracking_start_time = datetime.now(
499
+ timezone.utc
500
+ ) # Store as datetime object for UTC
501
+
502
+ self._track_aliases: Dict[Any, Any] = {}
503
+ self._canonical_tracks: Dict[Any, Dict[str, Any]] = {}
504
+ # Tunable parameters – adjust if necessary for specific scenarios
505
+ self._track_merge_iou_threshold: float = 0.05 # IoU ≥ 0.05 →
506
+ self._track_merge_time_window: float = 7.0 # seconds within which to merge
507
+
508
+ self._ascending_alert_list: List[int] = []
509
+ self.current_incident_end_timestamp: str = "N/A"
510
+
511
+ # Session totals tracked per unique internal track id (thread-safe)
512
+ self._recognized_track_ids = set()
513
+ self._unknown_track_ids = set()
514
+ self._tracking_lock = threading.Lock()
515
+
516
+ # Person tracking: {person_id: [{"camera_id": str, "timestamp": str}, ...]}
517
+ self.person_tracking: Dict[str, List[Dict[str, str]]] = {}
518
+
519
+ self.face_client = None
520
+
521
+ # Initialize PeopleActivityLogging without face client initially
522
+ self.people_activity_logging = None
523
+
524
+ # Initialize EmbeddingManager - will be configured in process method
525
+ self.embedding_manager = None
526
+ # Temporal identity manager for API-based top-1 identity smoothing
527
+ self.temporal_identity_manager = None
528
+ # Removed lightweight face tracker fallback; we always use AdvancedTracker
529
+ # Optional gating similar to compare_similarity
530
+ self._track_first_seen: Dict[int, int] = {}
531
+ self._probation_frames: int = 260 # default gate ~4 seconds at 60 fps; tune per stream
532
+ self._min_face_w: int = 30
533
+ self._min_face_h: int = 30
534
+
535
+ self.start_timer = None
536
+
537
+ # Store config for async initialization
538
+ self._default_config = config
539
+ self._initialized = False
540
+
541
+ # Don't call asyncio.run() in __init__ - it will fail if called from async context
542
+ # Initialization must be done by calling await initialize(config) after instantiation
543
+ # This is handled in PostProcessor._get_use_case_instance()
544
+
545
+ async def initialize(self, config: Optional[FaceRecognitionEmbeddingConfig] = None) -> None:
546
+ """
547
+ Async initialization method to set up face client and all components.
548
+ Must be called after __init__ before process() can be called.
549
+
550
+ CRITICAL INITIALIZATION SEQUENCE:
551
+ 1. Initialize face client and update deployment
552
+ 2. Create EmbeddingManager (does NOT load embeddings yet)
553
+ 3. Synchronously load embeddings with _load_staff_embeddings() - MUST succeed
554
+ 4. Verify embeddings are actually loaded (fail-fast if not)
555
+ 5. Start background refresh thread (only after successful load)
556
+ 6. Initialize TemporalIdentityManager with loaded EmbeddingManager
557
+ 7. Final verification of all components
558
+
559
+ This sequence ensures:
560
+ - No race conditions between main load and background thread
561
+ - Fail-fast behavior if embeddings can't be loaded
562
+ - All components have verified embeddings before use
563
+
564
+ Args:
565
+ config: Optional config to use. If not provided, uses config from __init__.
566
+
567
+ Raises:
568
+ RuntimeError: If embeddings fail to load or verification fails
569
+ """
570
+ print("=============== INITIALIZE() CALLED ===============")
571
+ if self._initialized:
572
+ self.logger.debug("Use case already initialized, skipping")
573
+ print("=============== ALREADY INITIALIZED, SKIPPING ===============")
574
+ return
575
+
576
+ # Use provided config or fall back to default config from __init__
577
+ init_config = config or self._default_config
578
+
579
+ if not init_config:
580
+ raise ValueError("No config provided for initialization - config is required")
581
+
582
+ # Validate config type
583
+ if not isinstance(init_config, FaceRecognitionEmbeddingConfig):
584
+ raise TypeError(f"Invalid config type for initialization: {type(init_config)}, expected FaceRecognitionEmbeddingConfig")
585
+
586
+ self.logger.info("Initializing face recognition use case with provided config")
587
+ # print("=============== STEP 1: INITIALIZING FACE CLIENT ===============")
588
+
589
+ # Initialize face client (includes deployment update)
590
+ try:
591
+ self.face_client = await self._get_facial_recognition_client(init_config)
592
+ # print(f"=============== FACE CLIENT INITIALIZED: {self.face_client is not None} ===============")
593
+
594
+ # Initialize People activity logging if enabled
595
+ if init_config.enable_people_activity_logging:
596
+ self.people_activity_logging = PeopleActivityLogging(self.face_client)
597
+ # PeopleActivityLogging starts its background thread in __init__
598
+ self.logger.info("People activity logging enabled and started")
599
+
600
+ # Initialize EmbeddingManager
601
+ # print("=============== STEP 2: INITIALIZING EMBEDDING MANAGER ===============")
602
+ if not init_config.embedding_config:
603
+ # print("=============== CREATING EMBEDDING CONFIG ===============")
604
+ init_config.embedding_config = EmbeddingConfig(
605
+ similarity_threshold=init_config.similarity_threshold,
606
+ confidence_threshold=init_config.confidence_threshold,
607
+ enable_track_id_cache=init_config.enable_track_id_cache,
608
+ cache_max_size=init_config.cache_max_size,
609
+ cache_ttl=3600,
610
+ background_refresh_interval=43200,
611
+ staff_embeddings_cache_ttl=43200,
612
+ )
613
+ self.embedding_manager = EmbeddingManager(init_config.embedding_config, self.face_client)
614
+ # print(f"=============== EMBEDDING MANAGER CREATED: {self.embedding_manager is not None} ===============")
615
+ self.logger.info("Embedding manager initialized")
616
+
617
+ # Load staff embeddings immediately for fast startup (avoid race conditions)
618
+ # This MUST succeed before we can proceed - fail fast if it doesn't
619
+ # print("=============== STEP 3: CALLING _load_staff_embeddings() ===============")
620
+ embeddings_loaded = await self.embedding_manager._load_staff_embeddings()
621
+ # print(f"=============== EMBEDDINGS LOADED: {embeddings_loaded} ===============")
622
+
623
+ if not embeddings_loaded:
624
+ error_msg = "CRITICAL: Failed to load staff embeddings at initialization - cannot proceed without embeddings"
625
+ print(f"=============== {error_msg} ===============")
626
+ self.logger.error(error_msg)
627
+ raise RuntimeError(error_msg)
628
+
629
+ # Verify embeddings are actually loaded using is_ready() method
630
+ if not self.embedding_manager.is_ready():
631
+ status = self.embedding_manager.get_status()
632
+ error_msg = f"CRITICAL: Embeddings not ready after load - status: {status}"
633
+ print(f"=============== {error_msg} ===============")
634
+ self.logger.error(error_msg)
635
+ raise RuntimeError(error_msg)
636
+
637
+ # print(f"=============== STAFF EMBEDDINGS COUNT: {len(self.embedding_manager.staff_embeddings)} ===============")
638
+ # print(f"=============== EMBEDDINGS MATRIX SHAPE: {self.embedding_manager.embeddings_matrix.shape} ===============")
639
+ # print(f"=============== EMBEDDINGS LOADED FLAG: {self.embedding_manager._embeddings_loaded} ===============")
640
+ self.logger.info(f"Successfully loaded {len(self.embedding_manager.staff_embeddings)} staff embeddings at initialization")
641
+
642
+ # NOW start background refresh after successful initial load (prevents race conditions)
643
+ if init_config.embedding_config.enable_background_refresh:
644
+ # print("=============== STEP 4: STARTING BACKGROUND REFRESH ===============")
645
+ self.embedding_manager.start_background_refresh()
646
+ self.logger.info("Background embedding refresh started after successful initial load")
647
+
648
+ # Initialize TemporalIdentityManager with EmbeddingManager for fast local search
649
+ # print("=============== STEP 5: INITIALIZING TEMPORAL IDENTITY MANAGER ===============")
650
+ self.temporal_identity_manager = TemporalIdentityManager(
651
+ face_client=self.face_client,
652
+ embedding_manager=self.embedding_manager,
653
+ recognition_threshold=float(init_config.similarity_threshold),
654
+ history_size=20,
655
+ unknown_patience=7,
656
+ switch_patience=5,
657
+ fallback_margin=0.05,
658
+ )
659
+ self.logger.info("Temporal identity manager initialized with embedding manager for local similarity search")
660
+
661
+ # Final verification before marking as initialized
662
+ # print("=============== STEP 6: FINAL VERIFICATION ===============")
663
+ if not self.embedding_manager.is_ready():
664
+ status = self.embedding_manager.get_status()
665
+ error_msg = f"CRITICAL: Final verification failed - embeddings not ready. Status: {status}"
666
+ print(f"=============== {error_msg} ===============")
667
+ self.logger.error(error_msg)
668
+ raise RuntimeError(error_msg)
669
+
670
+ # Log detailed status for debugging
671
+ status = self.embedding_manager.get_status()
672
+ # print(f"=============== FINAL CHECKS PASSED ===============")
673
+ # print(f" - Face client: {self.face_client is not None}")
674
+ # print(f" - Embedding manager: {self.embedding_manager is not None}")
675
+ # print(f" - Embedding manager status: {status}")
676
+ # print(f" - Temporal identity manager: {self.temporal_identity_manager is not None}")
677
+
678
+ self._initialized = True
679
+ self.logger.info("Face recognition use case fully initialized and verified")
680
+ # print("=============== INITIALIZATION COMPLETE ===============")
681
+
682
+ except Exception as e:
683
+ self.logger.error(f"Error during use case initialization: {e}", exc_info=True)
684
+ raise RuntimeError(f"Failed to initialize face recognition use case: {e}") from e
685
+
686
+ async def _get_facial_recognition_client(
687
+ self, config: FaceRecognitionEmbeddingConfig
688
+ ) -> FacialRecognitionClient:
689
+ """Get facial recognition client and update deployment"""
690
+ # Initialize face recognition client if not already done
691
+ if self.face_client is None:
692
+ self.logger.info(
693
+ f"Initializing face recognition client with server ID: {config.facial_recognition_server_id}"
694
+ )
695
+ self.face_client = FacialRecognitionClient(
696
+ server_id=config.facial_recognition_server_id, session=config.session
697
+ )
698
+ self.logger.info("Face recognition client initialized")
699
+
700
+ # Call update_deployment if deployment_id is provided
701
+ if config.deployment_id:
702
+ try:
703
+ self.logger.info(f"Updating deployment with ID: {config.deployment_id}")
704
+ response = await self.face_client.update_deployment(config.deployment_id)
705
+ if response.get('success', False):
706
+ self.logger.info(f"Successfully updated deployment {config.deployment_id}")
707
+ else:
708
+ self.logger.warning(f"Failed to update deployment: {response.get('error', 'Unknown error')}")
709
+ except Exception as e:
710
+ self.logger.error(f"Exception while updating deployment: {e}", exc_info=True)
711
+ else:
712
+ self.logger.debug("No deployment_id provided, skipping deployment update")
713
+
714
+ return self.face_client
715
+
716
+ async def process(
717
+ self,
718
+ data: Any,
719
+ config: ConfigProtocol,
720
+ input_bytes: Optional[bytes] = None,
721
+ context: Optional[ProcessingContext] = None,
722
+ stream_info: Optional[Dict[str, Any]] = None,
723
+ ) -> ProcessingResult:
724
+ """
725
+ Main entry point for face recognition with embeddings post-processing.
726
+ Applies all standard processing plus face recognition and auto-enrollment.
727
+
728
+ Thread-safe: Uses local variables for per-request state and locks for global totals.
729
+ Order-preserving: Processes detections sequentially to maintain input order.
730
+ """
731
+ processing_start = time.time()
732
+ # Ensure config is correct type
733
+ if not isinstance(config, FaceRecognitionEmbeddingConfig):
734
+ return self.create_error_result(
735
+ "Invalid config type",
736
+ usecase=self.name,
737
+ category=self.category,
738
+ context=context,
739
+ )
740
+ if context is None:
741
+ context = ProcessingContext()
742
+
743
+ # Defensive check: Ensure context is ProcessingContext object (production safety)
744
+ # This handles edge cases where parameter mismatch might pass a dict as context
745
+ if not isinstance(context, ProcessingContext):
746
+ self.logger.warning(
747
+ f"Context parameter is not ProcessingContext (got {type(context).__name__}, {context}). "
748
+ "Creating new ProcessingContext. This may indicate a parameter mismatch in the caller."
749
+ )
750
+ context = ProcessingContext()
751
+
752
+ # Lazy initialization on first process() call (similar to tracker initialization pattern)
753
+ if not self._initialized:
754
+ self.logger.info("Initializing face recognition use case on first process() call...")
755
+ try:
756
+ await self.initialize(config)
757
+ self.logger.info("Face recognition use case initialized successfully")
758
+ except Exception as e:
759
+ self.logger.error(f"Failed to initialize face recognition use case: {e}", exc_info=True)
760
+ return self.create_error_result(
761
+ f"Initialization failed: {e}",
762
+ usecase=self.name,
763
+ category=self.category,
764
+ context=context,
765
+ )
766
+
767
+ # Ensure confidence threshold is set
768
+ if not config.confidence_threshold:
769
+ config.confidence_threshold = 0.35
770
+
771
+
772
+ # Detect input format and store in context
773
+ input_format = match_results_structure(data)
774
+ context.input_format = input_format
775
+
776
+ context.confidence_threshold = config.confidence_threshold
777
+
778
+ # Parse face recognition model output format (with embeddings)
779
+ processed_data = self._parse_face_model_output(data)
780
+ # Normalize embeddings early for consistency (local parity)
781
+ for _det in processed_data:
782
+ try:
783
+ emb = _det.get("embedding", []) or []
784
+ if emb:
785
+ _det["embedding"] = _normalize_embedding(emb)
786
+ except Exception:
787
+ pass
788
+ # Ignore any pre-existing track_id on detections (we rely on our own tracker)
789
+ for _det in processed_data:
790
+ if isinstance(_det, dict) and "track_id" in _det:
791
+ try:
792
+ del _det["track_id"]
793
+ except Exception:
794
+ _det["track_id"] = None
795
+
796
+ # Apply standard confidence filtering
797
+ if config.confidence_threshold is not None:
798
+ processed_data = filter_by_confidence(
799
+ processed_data, config.confidence_threshold
800
+ )
801
+ self.logger.debug(
802
+ f"Applied confidence filtering with threshold {config.confidence_threshold}"
803
+ )
804
+ else:
805
+ self.logger.debug(
806
+ "Did not apply confidence filtering since threshold not provided"
807
+ )
808
+
809
+ # Apply category mapping if provided
810
+ if config.index_to_category:
811
+ processed_data = apply_category_mapping(
812
+ processed_data, config.index_to_category
813
+ )
814
+ self.logger.debug("Applied category mapping")
815
+
816
+ # Apply category filtering
817
+ if config.target_categories:
818
+ processed_data = filter_by_categories(
819
+ processed_data, config.target_categories
820
+ )
821
+ self.logger.debug("Applied category filtering")
822
+
823
+ # print("------------------TILL TRACKER MS----------------------------")
824
+ # print(self._initialized,"LATENCY:",(time.time() - processing_start)*1000,"| Throughput fps:",(1.0 / (time.time() - processing_start)) if (time.time() - processing_start) > 0 else None)
825
+ # print("------------------TILL TRACKER MS----------------------------")
826
+ # Advanced tracking (BYTETracker-like) - only if enabled
827
+ if config.enable_face_tracking:
828
+ from ..advanced_tracker import AdvancedTracker
829
+ from ..advanced_tracker.config import TrackerConfig
830
+
831
+ # Create tracker instance if it doesn't exist (preserves state across frames)
832
+ if self.tracker is None:
833
+ tracker_config = TrackerConfig(
834
+ track_high_thresh=0.5,
835
+ track_low_thresh=0.05,
836
+ new_track_thresh=0.5,
837
+ match_thresh=0.8,
838
+ track_buffer=int(300), # allow short occlusions
839
+ max_time_lost=int(150),
840
+ fuse_score=True,
841
+ enable_gmc=False,
842
+ frame_rate=int(20)
843
+ )
844
+
845
+ self.tracker = AdvancedTracker(tracker_config)
846
+ self.logger.info(
847
+ "Initialized AdvancedTracker for Face Recognition with thresholds: "
848
+ f"high={tracker_config.track_high_thresh}, "
849
+ f"low={tracker_config.track_low_thresh}, "
850
+ f"new={tracker_config.new_track_thresh}"
851
+ )
852
+
853
+ # The tracker expects the data in the same format as input
854
+ # It will add track_id and frame_id to each detection (we rely ONLY on these)
855
+ processed_data = self.tracker.update(processed_data)
856
+ else:
857
+ self.logger.debug("Advanced face tracking disabled; continuing without external track IDs")
858
+
859
+ # Initialize local recognition summary variables
860
+ current_recognized_count = 0
861
+ current_unknown_count = 0
862
+ recognized_persons = {}
863
+ current_frame_staff_details = {}
864
+
865
+ # print("------------------TRACKER INIT END----------------------------")
866
+ # print("LATENCY:",(time.time() - processing_start)*1000,"| Throughput fps:",(1.0 / (time.time() - processing_start)) if (time.time() - processing_start) > 0 else None)
867
+ # print("------------------TRACKER INIT END----------------------------")
868
+
869
+ # Process face recognition for each detection (if enabled)
870
+ if config.enable_face_recognition:
871
+ # Additional safety check: verify embeddings are still loaded and ready
872
+ if not self.embedding_manager or not self.embedding_manager.is_ready():
873
+ status = self.embedding_manager.get_status() if self.embedding_manager else {}
874
+ error_msg = f"CRITICAL: Cannot process face recognition - embeddings not ready. Status: {status}"
875
+ self.logger.error(error_msg)
876
+ print(f"ERROR: {error_msg}")
877
+ return self.create_error_result(
878
+ error_msg,
879
+ usecase=self.name,
880
+ category=self.category,
881
+ context=context,
882
+ )
883
+
884
+ face_recognition_result = await self._process_face_recognition(
885
+ processed_data, config, stream_info, input_bytes
886
+ )
887
+ processed_data, current_recognized_count, current_unknown_count, recognized_persons, current_frame_staff_details = face_recognition_result
888
+ else:
889
+ # Just add default face recognition fields without actual recognition
890
+ for detection in processed_data:
891
+ detection["person_id"] = None
892
+ detection["person_name"] = "Unknown"
893
+ detection["recognition_status"] = "disabled"
894
+ detection["enrolled"] = False
895
+
896
+ # print("------------------FACE RECONG CONFIG ENABLED----------------------------")
897
+ # print("LATENCY:",(time.time() - processing_start)*1000,"| Throughput fps:",(1.0 / (time.time() - processing_start)) if (time.time() - processing_start) > 0 else None)
898
+ # print("------------------FACE RECONG CONFIG ENABLED----------------------------")
899
+
900
+ # Update tracking state for total count per label
901
+ self._update_tracking_state(processed_data)
902
+
903
+ # Update frame counter
904
+ self._total_frame_counter += 1
905
+
906
+ # Extract frame information from stream_info
907
+ frame_number = None
908
+ if stream_info:
909
+ input_settings = stream_info.get("input_settings", {})
910
+ start_frame = input_settings.get("start_frame")
911
+ end_frame = input_settings.get("end_frame")
912
+ # If start and end frame are the same, it's a single frame
913
+ if (
914
+ start_frame is not None
915
+ and end_frame is not None
916
+ and start_frame == end_frame
917
+ ):
918
+ frame_number = start_frame
919
+
920
+ # Compute summaries and alerts
921
+ general_counting_summary = calculate_counting_summary(data)
922
+ counting_summary = self._count_categories(processed_data, config)
923
+ # Add total unique counts after tracking using only local state
924
+ total_counts = self.get_total_counts()
925
+ counting_summary["total_counts"] = total_counts
926
+
927
+ # NEW: Add face recognition summary
928
+ counting_summary.update(self._get_face_recognition_summary(
929
+ current_recognized_count, current_unknown_count, recognized_persons
930
+ ))
931
+
932
+ # print("------------------TILL FACE RECOG SUMMARY----------------------------")
933
+ # print("LATENCY:",(time.time() - processing_start)*1000,"| Throughput fps:",(1.0 / (time.time() - processing_start)) if (time.time() - processing_start) > 0 else None)
934
+ # print("------------------TILL FACE RECOG SUMMARY----------------------------")
935
+
936
+ # Add detections to the counting summary (standard pattern for detection use cases)
937
+ # Ensure display label is present for UI (does not affect logic/counters)
938
+ for _d in processed_data:
939
+ if "display_name" not in _d:
940
+ name = _d.get("person_name")
941
+ # Use person_name only if recognized; otherwise leave empty to honor probation logic
942
+ _d["display_name"] = name if _d.get("recognition_status") == "known" else (_d.get("display_name", "") or "")
943
+ counting_summary["detections"] = processed_data
944
+
945
+ alerts = self._check_alerts(counting_summary, frame_number, config)
946
+
947
+ # Step: Generate structured incidents, tracking stats and business analytics with frame-based keys
948
+ incidents_list = self._generate_incidents(
949
+ counting_summary, alerts, config, frame_number, stream_info
950
+ )
951
+ tracking_stats_list = self._generate_tracking_stats(
952
+ counting_summary, alerts, config, frame_number, stream_info, current_frame_staff_details
953
+ )
954
+ business_analytics_list = self._generate_business_analytics(
955
+ counting_summary, alerts, config, stream_info, is_empty=True
956
+ )
957
+ summary_list = self._generate_summary(incidents_list, tracking_stats_list, business_analytics_list)
958
+
959
+ # Extract frame-based dictionaries from the lists
960
+ incidents = incidents_list[0] if incidents_list else {}
961
+ tracking_stats = tracking_stats_list[0] if tracking_stats_list else {}
962
+ business_analytics = (
963
+ business_analytics_list[0] if business_analytics_list else {}
964
+ )
965
+ summary = summary_list[0] if summary_list else {}
966
+
967
+ # print("------------------TILL TRACKING STATS----------------------------")
968
+ # print("LATENCY:",(time.time() - processing_start)*1000,"| Throughput fps:",(1.0 / (time.time() - processing_start)) if (time.time() - processing_start) > 0 else None)
969
+ # print("------------------TILL TRACKING STATS----------------------------")
970
+
971
+
972
+ agg_summary = {
973
+ str(frame_number): {
974
+ "incidents": incidents,
975
+ "tracking_stats": tracking_stats,
976
+ "business_analytics": business_analytics,
977
+ "alerts": alerts,
978
+ "human_text": summary,
979
+ "person_tracking": self.get_person_tracking_summary(),
980
+ }
981
+ }
982
+
983
+ context.mark_completed()
984
+
985
+ # Build result object following the standard pattern - same structure as people counting
986
+ result = self.create_result(
987
+ data={"agg_summary": agg_summary},
988
+ usecase=self.name,
989
+ category=self.category,
990
+ context=context,
991
+ )
992
+ proc_time = time.time() - processing_start
993
+ processing_latency_ms = proc_time * 1000.0
994
+ processing_fps = (1.0 / proc_time) if proc_time > 0 else None
995
+ # Log the performance metrics using the module-level logger
996
+ print("latency in ms:",processing_latency_ms,"| Throughput fps:",processing_fps,"| Frame_Number:",self._total_frame_counter)
997
+
998
+ return result
999
+
1000
+ def _parse_face_model_output(self, data: Any) -> List[Dict]:
1001
+ """Parse face recognition model output to standard detection format, preserving embeddings"""
1002
+ processed_data = []
1003
+
1004
+ if isinstance(data, dict):
1005
+ # Handle frame-based format: {"0": [...], "1": [...]}
1006
+ for frame_id, frame_detections in data.items():
1007
+ if isinstance(frame_detections, list):
1008
+ for detection in frame_detections:
1009
+ if isinstance(detection, dict):
1010
+ # Convert to standard format but preserve face-specific fields
1011
+ standard_detection = {
1012
+ "category": detection.get("category", "face"),
1013
+ "confidence": detection.get("confidence", 0.0),
1014
+ "bounding_box": detection.get("bounding_box", {}),
1015
+ "track_id": detection.get("track_id", ""),
1016
+ "frame_id": detection.get("frame_id", frame_id),
1017
+ # Preserve face-specific fields
1018
+ "embedding": detection.get("embedding", []),
1019
+ "landmarks": detection.get("landmarks", None),
1020
+ "fps": detection.get("fps", 30),
1021
+ }
1022
+ processed_data.append(standard_detection)
1023
+ elif isinstance(data, list):
1024
+ # Handle list format
1025
+ for detection in data:
1026
+ if isinstance(detection, dict):
1027
+ # Convert to standard format and ensure all required fields exist
1028
+ standard_detection = {
1029
+ "category": detection.get("category", "face"),
1030
+ "confidence": detection.get("confidence", 0.0),
1031
+ "bounding_box": detection.get("bounding_box", {}),
1032
+ "track_id": detection.get("track_id", ""),
1033
+ "frame_id": detection.get("frame_id", 0),
1034
+ # Preserve face-specific fields
1035
+ "embedding": detection.get("embedding", []),
1036
+ "landmarks": detection.get("landmarks", None),
1037
+ "fps": detection.get("fps", 30),
1038
+ "metadata": detection.get("metadata", {}),
1039
+ }
1040
+ processed_data.append(standard_detection)
1041
+
1042
+ return processed_data
1043
+
1044
+ def _extract_frame_from_data(self, input_bytes: bytes) -> Optional[np.ndarray]:
1045
+ """
1046
+ Extract frame from original model data
1047
+
1048
+ Args:
1049
+ original_data: Original data from model (same format as model receives)
1050
+
1051
+ Returns:
1052
+ np.ndarray: Frame data or None if not found
1053
+ """
1054
+ try:
1055
+ try:
1056
+ if isinstance(input_bytes, str):
1057
+ frame_bytes = base64.b64decode(input_bytes)
1058
+ else:
1059
+ frame_bytes = input_bytes
1060
+ nparr = np.frombuffer(frame_bytes, np.uint8)
1061
+ frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
1062
+ return frame
1063
+ except Exception as e:
1064
+ self.logger.debug(f"Could not decode direct frame data: {e}")
1065
+
1066
+ return None
1067
+
1068
+ except Exception as e:
1069
+ self.logger.debug(f"Error extracting frame from data: {e}")
1070
+ return None
1071
+
1072
+ # Removed unused _calculate_bbox_area_percentage (not referenced)
1073
+
1074
+ async def _process_face_recognition(
1075
+ self,
1076
+ detections: List[Dict],
1077
+ config: FaceRecognitionEmbeddingConfig,
1078
+ stream_info: Optional[Dict[str, Any]] = None,
1079
+ input_bytes: Optional[bytes] = None,
1080
+ ) -> List[Dict]:
1081
+ """Process face recognition for each detection with embeddings"""
1082
+
1083
+ # Face client is initialized in initialize(); if absent, this indicates a prior init failure
1084
+ if not self.face_client:
1085
+ self.logger.error("Face client not initialized; initialize() must succeed before processing")
1086
+ return []
1087
+
1088
+ # Initialize unknown faces storage if not exists
1089
+ if not hasattr(self, "unknown_faces_storage"):
1090
+ self.unknown_faces_storage = {}
1091
+
1092
+ # Initialize frame availability warning flag to avoid spam
1093
+ if not hasattr(self, "_frame_warning_logged"):
1094
+ self._frame_warning_logged = False
1095
+
1096
+ # Initialize per-request tracking (thread-safe)
1097
+ current_recognized_count = 0
1098
+ current_unknown_count = 0
1099
+ recognized_persons = {}
1100
+ current_frame_staff_details = {} # Store staff details for current frame
1101
+
1102
+ # Extract frame from original data for cropping unknown faces
1103
+ current_frame = (
1104
+ self._extract_frame_from_data(input_bytes) if input_bytes else None
1105
+ )
1106
+
1107
+ # Log frame availability once per session
1108
+ if current_frame is None and not self._frame_warning_logged:
1109
+ if config.enable_unknown_face_processing:
1110
+ self.logger.info(
1111
+ "Frame data not available in model output - unknown face cropping/uploading will be skipped. "
1112
+ "To disable this feature entirely, set enable_unknown_face_processing=False"
1113
+ )
1114
+ self._frame_warning_logged = True
1115
+
1116
+ # Get location from stream_info
1117
+ location = (
1118
+ stream_info.get("camera_location", "unknown") if stream_info else "unknown"
1119
+ )
1120
+
1121
+ # Generate current timestamp
1122
+ current_timestamp = datetime.now(timezone.utc).isoformat()
1123
+
1124
+ final_detections = []
1125
+ # Process detections sequentially to preserve order
1126
+ for detection in detections:
1127
+
1128
+ # Process each detection sequentially with await to preserve order
1129
+ st1=time.time()
1130
+ processed_detection = await self._process_face(
1131
+ detection, current_frame, location, current_timestamp, config,
1132
+ current_recognized_count, current_unknown_count,
1133
+ recognized_persons, current_frame_staff_details
1134
+ )
1135
+ # print("------------------WHOLE FACE RECOG PROCESSING DETECTION----------------------------")
1136
+ # print("LATENCY:",(time.time() - st1)*1000,"| Throughput fps:",(1.0 / (time.time() - st1)) if (time.time() - st1) > 0 else None)
1137
+ # print("------------------WHOLE FACE RECOG PROCESSING DETECTION----------------------------")
1138
+
1139
+ # Include both known and unknown faces in final detections (maintains original order)
1140
+ if processed_detection:
1141
+ final_detections.append(processed_detection)
1142
+ # Update local counters based on processed detection
1143
+ if processed_detection.get("recognition_status") == "known":
1144
+ staff_id = processed_detection.get("person_id")
1145
+ if staff_id:
1146
+ current_frame_staff_details[staff_id] = processed_detection.get("person_name", "Unknown")
1147
+ current_recognized_count += 1
1148
+ recognized_persons[staff_id] = recognized_persons.get(staff_id, 0) + 1
1149
+ elif processed_detection.get("recognition_status") == "unknown":
1150
+ current_unknown_count += 1
1151
+
1152
+ return final_detections, current_recognized_count, current_unknown_count, recognized_persons, current_frame_staff_details
1153
+
1154
+ async def _process_face(
1155
+ self,
1156
+ detection: Dict,
1157
+ current_frame: np.ndarray,
1158
+ location: str = "",
1159
+ current_timestamp: str = "",
1160
+ config: FaceRecognitionEmbeddingConfig = None,
1161
+ current_recognized_count: int = 0,
1162
+ current_unknown_count: int = 0,
1163
+ recognized_persons: Dict = None,
1164
+ current_frame_staff_details: Dict = None,
1165
+ ) -> Dict:
1166
+
1167
+ # Extract and validate embedding using EmbeddingManager
1168
+ st2=time.time()
1169
+ detection, embedding = self.embedding_manager.extract_embedding_from_detection(detection)
1170
+ if not embedding:
1171
+ return None
1172
+
1173
+ # Internal tracker-provided ID (from AdvancedTracker; ignore upstream IDs entirely)
1174
+ track_id = detection.get("track_id")
1175
+ # print("------------------FACE RECOG EMBEDDING EXTRACTION----------------------------")
1176
+ # print("LATENCY:",(time.time() - st2)*1000,"| Throughput fps:",(1.0 / (time.time() - st2)) if (time.time() - st2) > 0 else None)
1177
+ # print("------------------FACE RECOG EMBEDDING EXTRACTION----------------------------")
1178
+
1179
+ # Determine if detection is eligible for recognition (similar to compare_similarity gating)
1180
+ bbox = detection.get("bounding_box", {}) or {}
1181
+ x1 = int(bbox.get("xmin", bbox.get("x1", 0)))
1182
+ y1 = int(bbox.get("ymin", bbox.get("y1", 0)))
1183
+ x2 = int(bbox.get("xmax", bbox.get("x2", 0)))
1184
+ y2 = int(bbox.get("ymax", bbox.get("y2", 0)))
1185
+ w_box = max(1, x2 - x1)
1186
+ h_box = max(1, y2 - y1)
1187
+ frame_id = detection.get("frame_id", None) #TODO: Maybe replace this with stream_info frame_id
1188
+
1189
+ # Track probation age strictly by internal tracker id
1190
+ if track_id is not None:
1191
+ if track_id not in self._track_first_seen:
1192
+ try:
1193
+ self._track_first_seen[track_id] = int(frame_id) if frame_id is not None else self._total_frame_counter
1194
+ except Exception:
1195
+ self._track_first_seen[track_id] = self._total_frame_counter
1196
+ age_frames = (int(frame_id) if frame_id is not None else self._total_frame_counter) - int(self._track_first_seen.get(track_id, 0)) + 1
1197
+ else:
1198
+ age_frames = 1
1199
+
1200
+ eligible_for_recognition = (w_box >= self._min_face_w and h_box >= self._min_face_h)
1201
+
1202
+ # Primary: API-based identity smoothing via TemporalIdentityManager
1203
+ staff_id = None
1204
+ person_name = "Unknown"
1205
+ similarity_score = 0.0
1206
+ employee_id = None
1207
+ staff_details: Dict[str, Any] = {}
1208
+ detection_type = "unknown"
1209
+ try:
1210
+ if self.temporal_identity_manager:
1211
+ track_key = track_id if track_id is not None else f"no_track_{id(detection)}"
1212
+ if not eligible_for_recognition:
1213
+ # Mirror compare_similarity: when not eligible, keep stable label if present
1214
+ s = self.temporal_identity_manager.tracks.get(track_key, {})
1215
+ if isinstance(s, dict):
1216
+ stable_staff_id = s.get("stable_staff_id")
1217
+ stable_person_name = s.get("stable_person_name") or "Unknown"
1218
+ stable_employee_id = s.get("stable_employee_id")
1219
+ stable_score = float(s.get("stable_score", 0.0))
1220
+ stable_staff_details = s.get("stable_staff_details") if isinstance(s.get("stable_staff_details"), dict) else {}
1221
+ if stable_staff_id is not None:
1222
+ staff_id = stable_staff_id
1223
+ person_name = stable_person_name
1224
+ employee_id = stable_employee_id
1225
+ similarity_score = stable_score
1226
+ staff_details = stable_staff_details
1227
+ detection_type = "known"
1228
+ else:
1229
+ detection_type = "unknown"
1230
+ # Also append embedding to history for temporal smoothing
1231
+ if embedding:
1232
+ try:
1233
+
1234
+ self.temporal_identity_manager._ensure_track(track_key)
1235
+ hist = self.temporal_identity_manager.tracks[track_key]["embedding_history"] # type: ignore
1236
+ hist.append(_normalize_embedding(embedding)) # type: ignore
1237
+ except Exception:
1238
+ pass
1239
+ else: #if eligible for recognition
1240
+ st3=time.time()
1241
+ staff_id, person_name, similarity_score, employee_id, staff_details, detection_type = await self.temporal_identity_manager.update(
1242
+ track_id=track_key,
1243
+ emb=embedding,
1244
+ eligible_for_recognition=True,
1245
+ location=location,
1246
+ timestamp=current_timestamp,
1247
+ )
1248
+ # print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE----------------------------")
1249
+ # print("LATENCY:",(time.time() - st3)*1000,"| Throughput fps:",(1.0 / (time.time() - st3)) if (time.time() - st3) > 0 else None)
1250
+ # print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE----------------------------")
1251
+ except Exception as e:
1252
+ self.logger.warning(f"TemporalIdentityManager update failed: {e}")
1253
+
1254
+ # # Fallback: if still unknown and we have an EmbeddingManager, use local search
1255
+ # if (staff_id is None or detection_type == "unknown") and self.embedding_manager is not None:
1256
+ # try:
1257
+ # search_result = await self.embedding_manager.search_face_embedding(
1258
+ # embedding=embedding,
1259
+ # track_id=track_id,
1260
+ # location=location,
1261
+ # timestamp=current_timestamp,
1262
+ # )
1263
+ # if search_result:
1264
+ # employee_id = search_result.employee_id
1265
+ # staff_id = search_result.staff_id
1266
+ # detection_type = search_result.detection_type
1267
+ # staff_details = search_result.staff_details
1268
+ # person_name = search_result.person_name
1269
+ # similarity_score = search_result.similarity_score
1270
+ # except Exception as e:
1271
+ # self.logger.warning(f"Local embedding search fallback failed: {e}")
1272
+
1273
+ # Update detection object directly (avoid relying on SearchResult type)
1274
+ detection = detection.copy()
1275
+ detection["person_id"] = staff_id
1276
+ detection["person_name"] = person_name or "Unknown"
1277
+ detection["recognition_status"] = "known" if staff_id else "unknown"
1278
+ detection["employee_id"] = employee_id
1279
+ detection["staff_details"] = staff_details if isinstance(staff_details, dict) else {}
1280
+ detection["similarity_score"] = float(similarity_score)
1281
+ detection["enrolled"] = bool(staff_id)
1282
+ # Display label policy: show only if identified OR probation exceeded, else empty label
1283
+ is_identified = (staff_id is not None and detection_type == "known")
1284
+ show_label = is_identified or (age_frames >= self._probation_frames and not is_identified)
1285
+ detection["display_name"] = (person_name if is_identified else ("Unknown" if show_label else ""))
1286
+ # Preserve original category (e.g., 'face') for tracking/counting
1287
+
1288
+ # Update global tracking per unique internal track id to avoid double-counting within a frame
1289
+ # Determine unknown strictly by recognition_status (display label never affects counters)
1290
+ is_truly_unknown = (detection.get("recognition_status") == "unknown")
1291
+
1292
+ try:
1293
+ internal_tid = detection.get("track_id")
1294
+ except Exception:
1295
+ internal_tid = None
1296
+
1297
+ if not is_truly_unknown and detection_type == "known":
1298
+ # Mark recognized and ensure it is not counted as unknown anymore
1299
+ self._track_person(staff_id)
1300
+ with self._tracking_lock:
1301
+ if internal_tid is not None:
1302
+ self._unknown_track_ids.discard(internal_tid)
1303
+ self._recognized_track_ids.add(internal_tid)
1304
+ else:
1305
+ # Only count as unknown in session totals if probation has been exceeded and still unknown
1306
+ matured_unknown = (age_frames >= self._probation_frames)
1307
+ if matured_unknown:
1308
+ with self._tracking_lock:
1309
+ if internal_tid is not None:
1310
+ # If it later becomes recognized, we'll remove it from unknown set above
1311
+ self._unknown_track_ids.add(internal_tid)
1312
+
1313
+ # Enqueue detection for background logging with all required parameters
1314
+ try:
1315
+ # Log known faces for activity tracking (skip any employee_id starting with "unknown_")
1316
+ if (
1317
+ detection["recognition_status"] == "known"
1318
+ and self.people_activity_logging
1319
+ and config
1320
+ and getattr(config, 'enable_people_activity_logging', True)
1321
+ and employee_id
1322
+ and not str(employee_id).startswith("unknown_")
1323
+ ):
1324
+ st4=time.time()
1325
+ await self.people_activity_logging.enqueue_detection(
1326
+ detection=detection,
1327
+ current_frame=current_frame,
1328
+ location=location,
1329
+ )
1330
+ # print("------------------FACE RECOG ENQUEUEING DETECTION FOR ACTIVITY LOGGING----------------------------")
1331
+ # print("LATENCY:",(time.time() - st4)*1000,"| Throughput fps:",(1.0 / (time.time() - st4)) if (time.time() - st4) > 0 else None)
1332
+ # print("------------------FACE RECOG ENQUEUEING DETECTION FOR ACTIVITY LOGGING----------------------------")
1333
+
1334
+ self.logger.debug(f"Enqueued known face detection for activity logging: {detection.get('person_name', 'Unknown')}")
1335
+ except Exception as e:
1336
+ self.logger.error(f"Error enqueueing detection for activity logging: {e}")
1337
+ # print("------------------PROCESS FACE LATENCY TOTAL----------------------------")
1338
+ print("LATENCY:",(time.time() - st2)*1000,"| Throughput fps:",(1.0 / (time.time() - st2)) if (time.time() - st2) > 0 else None)
1339
+ # print("------------------PROCESS FACE LATENCY TOTAL----------------------------")
1340
+
1341
+ return detection
1342
+
1343
+
1344
+
1345
+ def _return_error_detection(
1346
+ self,
1347
+ detection: Dict,
1348
+ person_id: str,
1349
+ person_name: str,
1350
+ recognition_status: str,
1351
+ enrolled: bool,
1352
+ category: str,
1353
+ error: str,
1354
+ ) -> Dict:
1355
+ """Return error detection"""
1356
+ detection["person_id"] = person_id
1357
+ detection["person_name"] = person_name
1358
+ detection["recognition_status"] = recognition_status
1359
+ detection["enrolled"] = enrolled
1360
+ detection["category"] = category
1361
+ detection["error"] = error
1362
+ return detection
1363
+
1364
+ def _track_person(self, person_id: str) -> None:
1365
+ """Track person with camera ID and UTC timestamp"""
1366
+ if person_id not in self.person_tracking:
1367
+ self.person_tracking[person_id] = []
1368
+
1369
+ # Add current detection
1370
+ detection_record = {
1371
+ "camera_id": "test_camera_001", # TODO: Get from stream_info in production
1372
+ "timestamp": datetime.utcnow().isoformat() + "Z",
1373
+ }
1374
+ self.person_tracking[person_id].append(detection_record)
1375
+
1376
+ def get_person_tracking_summary(self) -> Dict:
1377
+ """Get summary of tracked persons with camera IDs and timestamps"""
1378
+ return dict(self.person_tracking)
1379
+
1380
+ def get_unknown_faces_storage(self) -> Dict[str, bytes]:
1381
+ """Get stored unknown face images as bytes"""
1382
+ if self.people_activity_logging:
1383
+ return self.people_activity_logging.get_unknown_faces_storage()
1384
+ return {}
1385
+
1386
+ def clear_unknown_faces_storage(self) -> None:
1387
+ """Clear stored unknown face images"""
1388
+ if self.people_activity_logging:
1389
+ self.people_activity_logging.clear_unknown_faces_storage()
1390
+
1391
+ def _get_face_recognition_summary(self, current_recognized_count: int, current_unknown_count: int, recognized_persons: Dict) -> Dict:
1392
+ """Get face recognition summary for current frame"""
1393
+ recognition_rate = 0.0
1394
+ total_current = current_recognized_count + current_unknown_count
1395
+ if total_current > 0:
1396
+ recognition_rate = (current_recognized_count / total_current) * 100
1397
+
1398
+ # Get thread-safe global totals
1399
+ with self._tracking_lock:
1400
+ total_recognized = len(self._recognized_track_ids)
1401
+ total_unknown = len(self._unknown_track_ids)
1402
+
1403
+ return {
1404
+ "face_recognition_summary": {
1405
+ "current_frame": {
1406
+ "recognized": current_recognized_count,
1407
+ "unknown": current_unknown_count,
1408
+ "total": total_current,
1409
+ "recognized_persons": dict(recognized_persons),
1410
+ "recognition_rate": round(recognition_rate, 1),
1411
+ },
1412
+ "session_totals": {
1413
+ "total_recognized": total_recognized,
1414
+ "total_unknown": total_unknown,
1415
+ "total_processed": total_recognized + total_unknown,
1416
+ },
1417
+ "person_tracking": self.get_person_tracking_summary(),
1418
+ }
1419
+ }
1420
+
1421
+ def _check_alerts(
1422
+ self, summary: dict, frame_number: Any, config: FaceRecognitionEmbeddingConfig
1423
+ ) -> List[Dict]:
1424
+ """
1425
+ Check if any alert thresholds are exceeded and return alert dicts.
1426
+ """
1427
+
1428
+ def get_trend(data, lookback=900, threshold=0.6):
1429
+ window = data[-lookback:] if len(data) >= lookback else data
1430
+ if len(window) < 2:
1431
+ return True # not enough data to determine trend
1432
+ increasing = 0
1433
+ total = 0
1434
+ for i in range(1, len(window)):
1435
+ if window[i] >= window[i - 1]:
1436
+ increasing += 1
1437
+ total += 1
1438
+ ratio = increasing / total
1439
+ if ratio >= threshold:
1440
+ return True
1441
+ elif ratio <= (1 - threshold):
1442
+ return False
1443
+
1444
+ frame_key = str(frame_number) if frame_number is not None else "current_frame"
1445
+ alerts = []
1446
+ total_detections = summary.get("total_count", 0)
1447
+ face_summary = summary.get("face_recognition_summary", {})
1448
+ current_unknown = face_summary.get("current_frame", {}).get("unknown", 0)
1449
+
1450
+ if not config.alert_config:
1451
+ return alerts
1452
+
1453
+ if (
1454
+ hasattr(config.alert_config, "count_thresholds")
1455
+ and config.alert_config.count_thresholds
1456
+ ):
1457
+ for category, threshold in config.alert_config.count_thresholds.items():
1458
+ if category == "unknown_faces" and current_unknown > threshold:
1459
+ alerts.append(
1460
+ {
1461
+ "alert_type": (
1462
+ getattr(config.alert_config, "alert_type", ["Default"])
1463
+ if hasattr(config.alert_config, "alert_type")
1464
+ else ["Default"]
1465
+ ),
1466
+ "alert_id": f"alert_unknown_faces_{frame_key}",
1467
+ "incident_category": "unknown_face_detection",
1468
+ "threshold_level": threshold,
1469
+ "current_count": current_unknown,
1470
+ "ascending": get_trend(
1471
+ self._ascending_alert_list, lookback=900, threshold=0.8
1472
+ ),
1473
+ "settings": {
1474
+ t: v
1475
+ for t, v in zip(
1476
+ (
1477
+ getattr(
1478
+ config.alert_config,
1479
+ "alert_type",
1480
+ ["Default"],
1481
+ )
1482
+ if hasattr(config.alert_config, "alert_type")
1483
+ else ["Default"]
1484
+ ),
1485
+ (
1486
+ getattr(
1487
+ config.alert_config, "alert_value", ["JSON"]
1488
+ )
1489
+ if hasattr(config.alert_config, "alert_value")
1490
+ else ["JSON"]
1491
+ ),
1492
+ )
1493
+ },
1494
+ }
1495
+ )
1496
+ elif category == "all" and total_detections > threshold:
1497
+ alerts.append(
1498
+ {
1499
+ "alert_type": (
1500
+ getattr(config.alert_config, "alert_type", ["Default"])
1501
+ if hasattr(config.alert_config, "alert_type")
1502
+ else ["Default"]
1503
+ ),
1504
+ "alert_id": "alert_" + category + "_" + frame_key,
1505
+ "incident_category": self.CASE_TYPE,
1506
+ "threshold_level": threshold,
1507
+ "ascending": get_trend(
1508
+ self._ascending_alert_list, lookback=900, threshold=0.8
1509
+ ),
1510
+ "settings": {
1511
+ t: v
1512
+ for t, v in zip(
1513
+ (
1514
+ getattr(
1515
+ config.alert_config,
1516
+ "alert_type",
1517
+ ["Default"],
1518
+ )
1519
+ if hasattr(config.alert_config, "alert_type")
1520
+ else ["Default"]
1521
+ ),
1522
+ (
1523
+ getattr(
1524
+ config.alert_config, "alert_value", ["JSON"]
1525
+ )
1526
+ if hasattr(config.alert_config, "alert_value")
1527
+ else ["JSON"]
1528
+ ),
1529
+ )
1530
+ },
1531
+ }
1532
+ )
1533
+
1534
+ return alerts
1535
+
1536
+ def _generate_tracking_stats(
1537
+ self,
1538
+ counting_summary: Dict,
1539
+ alerts: List,
1540
+ config: FaceRecognitionEmbeddingConfig,
1541
+ frame_number: Optional[int] = None,
1542
+ stream_info: Optional[Dict[str, Any]] = None,
1543
+ current_frame_staff_details: Dict = None,
1544
+ ) -> List[Dict]:
1545
+ """Generate structured tracking stats matching eg.json format with face recognition data."""
1546
+ camera_info = self.get_camera_info_from_stream(stream_info)
1547
+ tracking_stats = []
1548
+
1549
+ total_detections = counting_summary.get("total_count", 0)
1550
+ total_counts_dict = counting_summary.get("total_counts", {})
1551
+ cumulative_total = sum(total_counts_dict.values()) if total_counts_dict else 0
1552
+ per_category_count = counting_summary.get("per_category_count", {})
1553
+ face_summary = counting_summary.get("face_recognition_summary", {})
1554
+
1555
+ current_timestamp = self._get_current_timestamp_str(
1556
+ stream_info, precision=False
1557
+ )
1558
+ start_timestamp = self._get_start_timestamp_str(stream_info, precision=False)
1559
+
1560
+ # Create high precision timestamps for input_timestamp and reset_timestamp
1561
+ high_precision_start_timestamp = self._get_current_timestamp_str(
1562
+ stream_info, precision=True
1563
+ )
1564
+ high_precision_reset_timestamp = self._get_start_timestamp_str(
1565
+ stream_info, precision=True
1566
+ )
1567
+
1568
+ # Build total_counts array in expected format
1569
+ total_counts = []
1570
+ for cat, count in total_counts_dict.items():
1571
+ if count > 0:
1572
+ total_counts.append({"category": cat, "count": count})
1573
+
1574
+ # Add face recognition specific total counts
1575
+ session_totals = face_summary.get("session_totals", {})
1576
+ total_counts.extend(
1577
+ [
1578
+ {
1579
+ "category": "recognized_faces",
1580
+ "count": session_totals.get("total_recognized", 0),
1581
+ },
1582
+ {
1583
+ "category": "unknown_faces",
1584
+ "count": session_totals.get("total_unknown", 0),
1585
+ },
1586
+ ]
1587
+ )
1588
+
1589
+ # Build current_counts array in expected format
1590
+ current_counts = []
1591
+ for cat, count in per_category_count.items():
1592
+ if count > 0 or total_detections > 0:
1593
+ current_counts.append({"category": cat, "count": count})
1594
+
1595
+ # Add face recognition specific current counts
1596
+ current_frame = face_summary.get("current_frame", {})
1597
+ current_counts.extend(
1598
+ [
1599
+ {
1600
+ "category": "recognized_faces",
1601
+ "count": current_frame.get("recognized", 0),
1602
+ },
1603
+ {"category": "unknown_faces", "count": current_frame.get("unknown", 0)},
1604
+ ]
1605
+ )
1606
+
1607
+ # Prepare detections with face recognition info
1608
+ detections = []
1609
+ for detection in counting_summary.get("detections", []):
1610
+ bbox = detection.get("bounding_box", {})
1611
+ category = detection.get("display_name", "")
1612
+
1613
+ detection_obj = self.create_detection_object(category, bbox)
1614
+ # Add face recognition specific fields
1615
+ detection_obj.update(
1616
+ {
1617
+ "person_id": detection.get("person_id"),
1618
+ # Use display_name for front-end label suppression policy
1619
+ "person_name": detection.get("display_name", ""),
1620
+ # Explicit label field for UI overlays
1621
+ "label": detection.get("display_name", ""),
1622
+ "recognition_status": detection.get(
1623
+ "recognition_status", "unknown"
1624
+ ),
1625
+ "enrolled": detection.get("enrolled", False),
1626
+ }
1627
+ )
1628
+ detections.append(detection_obj)
1629
+
1630
+ # Build alert_settings array in expected format
1631
+ alert_settings = []
1632
+ if config.alert_config and hasattr(config.alert_config, "alert_type"):
1633
+ alert_settings.append(
1634
+ {
1635
+ "alert_type": (
1636
+ getattr(config.alert_config, "alert_type", ["Default"])
1637
+ if hasattr(config.alert_config, "alert_type")
1638
+ else ["Default"]
1639
+ ),
1640
+ "incident_category": self.CASE_TYPE,
1641
+ "threshold_level": (
1642
+ config.alert_config.count_thresholds
1643
+ if hasattr(config.alert_config, "count_thresholds")
1644
+ else {}
1645
+ ),
1646
+ "ascending": True,
1647
+ "settings": {
1648
+ t: v
1649
+ for t, v in zip(
1650
+ (
1651
+ getattr(config.alert_config, "alert_type", ["Default"])
1652
+ if hasattr(config.alert_config, "alert_type")
1653
+ else ["Default"]
1654
+ ),
1655
+ (
1656
+ getattr(config.alert_config, "alert_value", ["JSON"])
1657
+ if hasattr(config.alert_config, "alert_value")
1658
+ else ["JSON"]
1659
+ ),
1660
+ )
1661
+ },
1662
+ }
1663
+ )
1664
+
1665
+
1666
+ human_text_lines = [f"CURRENT FRAME @ {current_timestamp}"]
1667
+
1668
+ current_recognized = current_frame.get("recognized", 0)
1669
+ current_unknown = current_frame.get("unknown", 0)
1670
+ recognized_persons = current_frame.get("recognized_persons", {})
1671
+ total_current = current_recognized + current_unknown
1672
+
1673
+ # Show staff names and IDs being recognized in current frame (with tabs)
1674
+ human_text_lines.append(f"\tCurrent Total Faces: {total_current}")
1675
+ human_text_lines.append(f"\tCurrent Recognized: {current_recognized}")
1676
+
1677
+ if recognized_persons:
1678
+ for person_id in recognized_persons.keys():
1679
+ # Get actual staff name from current frame processing
1680
+ staff_name = (current_frame_staff_details or {}).get(
1681
+ person_id, f"Staff {person_id}"
1682
+ )
1683
+ human_text_lines.append(f"\tName: {staff_name} (ID: {person_id})")
1684
+ human_text_lines.append(f"\tCurrent Unknown: {current_unknown}")
1685
+
1686
+ # Show current frame counts only (with tabs)
1687
+ human_text_lines.append("")
1688
+ human_text_lines.append(f"TOTAL SINCE @ {start_timestamp}")
1689
+ human_text_lines.append(f"\tTotal Faces: {cumulative_total}")
1690
+ human_text_lines.append(f"\tRecognized: {face_summary.get('session_totals',{}).get('total_recognized', 0)}")
1691
+ human_text_lines.append(f"\tUnknown: {face_summary.get('session_totals',{}).get('total_unknown', 0)}")
1692
+ # Additional counts similar to compare_similarity HUD
1693
+ # try:
1694
+ # human_text_lines.append(f"\tCurrent Faces (detections): {total_detections}")
1695
+ # human_text_lines.append(f"\tTotal Unique Tracks: {cumulative_total}")
1696
+ # except Exception:
1697
+ # pass
1698
+
1699
+ human_text = "\n".join(human_text_lines)
1700
+
1701
+ if alerts:
1702
+ for alert in alerts:
1703
+ human_text_lines.append(
1704
+ f"Alerts: {alert.get('settings', {})} sent @ {current_timestamp}"
1705
+ )
1706
+ else:
1707
+ human_text_lines.append("Alerts: None")
1708
+
1709
+ human_text = "\n".join(human_text_lines)
1710
+ reset_settings = [
1711
+ {"interval_type": "daily", "reset_time": {"value": 9, "time_unit": "hour"}}
1712
+ ]
1713
+
1714
+ tracking_stat = self.create_tracking_stats(
1715
+ total_counts=total_counts,
1716
+ current_counts=current_counts,
1717
+ detections=detections,
1718
+ human_text=human_text,
1719
+ camera_info=camera_info,
1720
+ alerts=alerts,
1721
+ alert_settings=alert_settings,
1722
+ reset_settings=reset_settings,
1723
+ start_time=high_precision_start_timestamp,
1724
+ reset_time=high_precision_reset_timestamp,
1725
+ )
1726
+
1727
+ tracking_stats.append(tracking_stat)
1728
+ return tracking_stats
1729
+
1730
+ # Copy all other methods from face_recognition.py but add face recognition info to human text
1731
+ def _generate_incidents(
1732
+ self,
1733
+ counting_summary: Dict,
1734
+ alerts: List,
1735
+ config: FaceRecognitionEmbeddingConfig,
1736
+ frame_number: Optional[int] = None,
1737
+ stream_info: Optional[Dict[str, Any]] = None,
1738
+ ) -> List[Dict]:
1739
+ """Generate structured incidents for the output format with frame-based keys."""
1740
+
1741
+ incidents = []
1742
+ total_detections = counting_summary.get("total_count", 0)
1743
+ face_summary = counting_summary.get("face_recognition_summary", {})
1744
+ current_frame = face_summary.get("current_frame", {})
1745
+
1746
+ current_timestamp = self._get_current_timestamp_str(stream_info)
1747
+ camera_info = self.get_camera_info_from_stream(stream_info)
1748
+
1749
+ self._ascending_alert_list = (
1750
+ self._ascending_alert_list[-900:]
1751
+ if len(self._ascending_alert_list) > 900
1752
+ else self._ascending_alert_list
1753
+ )
1754
+
1755
+ if total_detections > 0:
1756
+ # Determine event level based on unknown faces ratio
1757
+ level = "low"
1758
+ intensity = 5.0
1759
+ start_timestamp = self._get_start_timestamp_str(stream_info)
1760
+ if start_timestamp and self.current_incident_end_timestamp == "N/A":
1761
+ self.current_incident_end_timestamp = "Incident still active"
1762
+ elif (
1763
+ start_timestamp
1764
+ and self.current_incident_end_timestamp == "Incident still active"
1765
+ ):
1766
+ if (
1767
+ len(self._ascending_alert_list) >= 15
1768
+ and sum(self._ascending_alert_list[-15:]) / 15 < 1.5
1769
+ ):
1770
+ self.current_incident_end_timestamp = current_timestamp
1771
+ elif (
1772
+ self.current_incident_end_timestamp != "Incident still active"
1773
+ and self.current_incident_end_timestamp != "N/A"
1774
+ ):
1775
+ self.current_incident_end_timestamp = "N/A"
1776
+
1777
+ # Base intensity on unknown faces
1778
+ current_unknown = current_frame.get("unknown", 0)
1779
+ unknown_ratio = (
1780
+ current_unknown / total_detections if total_detections > 0 else 0
1781
+ )
1782
+ intensity = min(10.0, unknown_ratio * 10 + (current_unknown / 3))
1783
+
1784
+ if intensity >= 9:
1785
+ level = "critical"
1786
+ self._ascending_alert_list.append(3)
1787
+ elif intensity >= 7:
1788
+ level = "significant"
1789
+ self._ascending_alert_list.append(2)
1790
+ elif intensity >= 5:
1791
+ level = "medium"
1792
+ self._ascending_alert_list.append(1)
1793
+ else:
1794
+ level = "low"
1795
+ self._ascending_alert_list.append(0)
1796
+
1797
+ # Generate human text in new format with face recognition info
1798
+ current_recognized = current_frame.get("recognized", 0)
1799
+ human_text_lines = [f"FACE RECOGNITION INCIDENTS @ {current_timestamp}:"]
1800
+ human_text_lines.append(f"\tSeverity Level: {(self.CASE_TYPE,level)}")
1801
+ human_text_lines.append(f"\tRecognized Faces: {current_recognized}")
1802
+ human_text_lines.append(f"\tUnknown Faces: {current_unknown}")
1803
+ human_text_lines.append(f"\tTotal Faces: {total_detections}")
1804
+ human_text = "\n".join(human_text_lines)
1805
+
1806
+ alert_settings = []
1807
+ if config.alert_config and hasattr(config.alert_config, "alert_type"):
1808
+ alert_settings.append(
1809
+ {
1810
+ "alert_type": (
1811
+ getattr(config.alert_config, "alert_type", ["Default"])
1812
+ if hasattr(config.alert_config, "alert_type")
1813
+ else ["Default"]
1814
+ ),
1815
+ "incident_category": self.CASE_TYPE,
1816
+ "threshold_level": (
1817
+ config.alert_config.count_thresholds
1818
+ if hasattr(config.alert_config, "count_thresholds")
1819
+ else {}
1820
+ ),
1821
+ "ascending": True,
1822
+ "settings": {
1823
+ t: v
1824
+ for t, v in zip(
1825
+ (
1826
+ getattr(
1827
+ config.alert_config, "alert_type", ["Default"]
1828
+ )
1829
+ if hasattr(config.alert_config, "alert_type")
1830
+ else ["Default"]
1831
+ ),
1832
+ (
1833
+ getattr(
1834
+ config.alert_config, "alert_value", ["JSON"]
1835
+ )
1836
+ if hasattr(config.alert_config, "alert_value")
1837
+ else ["JSON"]
1838
+ ),
1839
+ )
1840
+ },
1841
+ }
1842
+ )
1843
+
1844
+ event = self.create_incident(
1845
+ incident_id=self.CASE_TYPE + "_" + str(frame_number),
1846
+ incident_type=self.CASE_TYPE,
1847
+ severity_level=level,
1848
+ human_text=human_text,
1849
+ camera_info=camera_info,
1850
+ alerts=alerts,
1851
+ alert_settings=alert_settings,
1852
+ start_time=start_timestamp,
1853
+ end_time=self.current_incident_end_timestamp,
1854
+ level_settings={"low": 1, "medium": 3, "significant": 4, "critical": 7},
1855
+ )
1856
+ incidents.append(event)
1857
+
1858
+ else:
1859
+ self._ascending_alert_list.append(0)
1860
+ incidents.append({})
1861
+
1862
+ return incidents
1863
+
1864
+ def _generate_business_analytics(
1865
+ self,
1866
+ counting_summary: Dict,
1867
+ alerts: Any,
1868
+ config: FaceRecognitionEmbeddingConfig,
1869
+ stream_info: Optional[Dict[str, Any]] = None,
1870
+ is_empty=False,
1871
+ ) -> List[Dict]:
1872
+ """Generate standardized business analytics for the agg_summary structure."""
1873
+ if is_empty:
1874
+ return []
1875
+ return []
1876
+
1877
+ def _generate_summary(self, incidents: List, tracking_stats: List, business_analytics: List) -> List[str]:
1878
+ """
1879
+ Generate a human_text string for the tracking_stat, incident, business analytics and alerts.
1880
+ """
1881
+ lines = []
1882
+ lines.append("Application Name: "+self.CASE_TYPE)
1883
+ lines.append("Application Version: "+self.CASE_VERSION)
1884
+ if len(incidents) > 0:
1885
+ lines.append("Incidents: "+f"\n\t{incidents[0].get('human_text', 'No incidents detected')}")
1886
+ if len(tracking_stats) > 0:
1887
+ lines.append("Tracking Statistics: "+f"\t{tracking_stats[0].get('human_text', 'No tracking statistics detected')}")
1888
+ if len(business_analytics) > 0:
1889
+ lines.append("Business Analytics: "+f"\t{business_analytics[0].get('human_text', 'No business analytics detected')}")
1890
+
1891
+ if len(incidents) == 0 and len(tracking_stats) == 0 and len(business_analytics) == 0:
1892
+ lines.append("Summary: "+"No Summary Data")
1893
+
1894
+ return ["\n".join(lines)]
1895
+
1896
+ # Include all the standard helper methods from face_recognition.py...
1897
+ def _count_categories(
1898
+ self, detections: list, config: FaceRecognitionEmbeddingConfig
1899
+ ) -> dict:
1900
+ """
1901
+ Count the number of detections per category and return a summary dict.
1902
+ The detections list is expected to have 'track_id' (from tracker), 'category', 'bounding_box', etc.
1903
+ Output structure will include 'track_id' for each detection as per AdvancedTracker output.
1904
+ """
1905
+ counts = {}
1906
+ for det in detections:
1907
+ cat = det.get("category", "unknown")
1908
+ counts[cat] = counts.get(cat, 0) + 1
1909
+ # Each detection dict will now include 'track_id' and face recognition fields
1910
+ return {
1911
+ "total_count": sum(counts.values()),
1912
+ "per_category_count": counts,
1913
+ "detections": [
1914
+ {
1915
+ "bounding_box": det.get("bounding_box"),
1916
+ "category": det.get("category"),
1917
+ "confidence": det.get("confidence"),
1918
+ "track_id": det.get("track_id"),
1919
+ "frame_id": det.get("frame_id"),
1920
+ # Face recognition fields
1921
+ "person_id": det.get("person_id"),
1922
+ "person_name": det.get("person_name"),
1923
+ "label": det.get("display_name", ""),
1924
+ "recognition_status": det.get("recognition_status"),
1925
+ "enrolled": det.get("enrolled"),
1926
+ "embedding": det.get("embedding", []),
1927
+ "landmarks": det.get("landmarks"),
1928
+ "staff_details": det.get(
1929
+ "staff_details"
1930
+ ), # Full staff information from API
1931
+ }
1932
+ for det in detections
1933
+ ],
1934
+ }
1935
+
1936
+ # Removed unused _extract_predictions (counts and outputs are built elsewhere)
1937
+
1938
+ # Copy all standard tracking, IoU, timestamp methods from face_recognition.py
1939
+ def _update_tracking_state(self, detections: list):
1940
+ """Track unique categories track_ids per category for total count after tracking."""
1941
+ if not hasattr(self, "_per_category_total_track_ids"):
1942
+ self._per_category_total_track_ids = {
1943
+ cat: set() for cat in self.target_categories
1944
+ }
1945
+ self._current_frame_track_ids = {cat: set() for cat in self.target_categories}
1946
+
1947
+ for det in detections:
1948
+ cat = det.get("category")
1949
+ raw_track_id = det.get("track_id")
1950
+ if cat not in self.target_categories or raw_track_id is None:
1951
+ continue
1952
+ bbox = det.get("bounding_box", det.get("bbox"))
1953
+ canonical_id = self._merge_or_register_track(raw_track_id, bbox)
1954
+ det["track_id"] = canonical_id
1955
+
1956
+ self._per_category_total_track_ids.setdefault(cat, set()).add(canonical_id)
1957
+ self._current_frame_track_ids[cat].add(canonical_id)
1958
+
1959
+ def get_total_counts(self):
1960
+ """Return total unique track_id count for each category."""
1961
+ return {
1962
+ cat: len(ids)
1963
+ for cat, ids in getattr(self, "_per_category_total_track_ids", {}).items()
1964
+ }
1965
+
1966
+ def _format_timestamp_for_stream(self, timestamp: float) -> str:
1967
+ """Format timestamp for streams (YYYY:MM:DD HH:MM:SS format)."""
1968
+ dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
1969
+ return dt.strftime("%Y:%m:%d %H:%M:%S")
1970
+
1971
+ def _format_timestamp_for_video(self, timestamp: float) -> str:
1972
+ """Format timestamp for video chunks (HH:MM:SS.ms format)."""
1973
+ hours = int(timestamp // 3600)
1974
+ minutes = int((timestamp % 3600) // 60)
1975
+ seconds = round(float(timestamp % 60), 2)
1976
+ return f"{hours:02d}:{minutes:02d}:{seconds:.1f}"
1977
+
1978
+ def _format_timestamp(self, timestamp: Any) -> str:
1979
+ """Format a timestamp to match the current timestamp format: YYYY:MM:DD HH:MM:SS.
1980
+
1981
+ The input can be either:
1982
+ 1. A numeric Unix timestamp (``float`` / ``int``) – it will be converted to datetime.
1983
+ 2. A string in the format ``YYYY-MM-DD-HH:MM:SS.ffffff UTC``.
1984
+
1985
+ The returned value will be in the format: YYYY:MM:DD HH:MM:SS (no milliseconds, no UTC suffix).
1986
+
1987
+ Example
1988
+ -------
1989
+ >>> self._format_timestamp("2025-10-27-19:31:20.187574 UTC")
1990
+ '2025:10:27 19:31:20'
1991
+ """
1992
+
1993
+ # Convert numeric timestamps to datetime first
1994
+ if isinstance(timestamp, (int, float)):
1995
+ dt = datetime.fromtimestamp(timestamp, timezone.utc)
1996
+ return dt.strftime('%Y:%m:%d %H:%M:%S')
1997
+
1998
+ # Ensure we are working with a string from here on
1999
+ if not isinstance(timestamp, str):
2000
+ return str(timestamp)
2001
+
2002
+ # Remove ' UTC' suffix if present
2003
+ timestamp_clean = timestamp.replace(' UTC', '').strip()
2004
+
2005
+ # Remove milliseconds if present (everything after the last dot)
2006
+ if '.' in timestamp_clean:
2007
+ timestamp_clean = timestamp_clean.split('.')[0]
2008
+
2009
+ # Parse the timestamp string and convert to desired format
2010
+ try:
2011
+ # Handle format: YYYY-MM-DD-HH:MM:SS
2012
+ if timestamp_clean.count('-') >= 2:
2013
+ # Replace first two dashes with colons for date part, third with space
2014
+ parts = timestamp_clean.split('-')
2015
+ if len(parts) >= 4:
2016
+ # parts = ['2025', '10', '27', '19:31:20']
2017
+ formatted = f"{parts[0]}:{parts[1]}:{parts[2]} {'-'.join(parts[3:])}"
2018
+ return formatted
2019
+ except Exception:
2020
+ pass
2021
+
2022
+ # If parsing fails, return the cleaned string as-is
2023
+ return timestamp_clean
2024
+
2025
+ def _get_current_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False, frame_id: Optional[str]=None) -> str:
2026
+ """Get formatted current timestamp based on stream type."""
2027
+
2028
+ if not stream_info:
2029
+ return "00:00:00.00"
2030
+ if precision:
2031
+ if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
2032
+ if frame_id:
2033
+ start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
2034
+ else:
2035
+ start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
2036
+ stream_time_str = self._format_timestamp_for_video(start_time)
2037
+
2038
+ return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
2039
+ else:
2040
+ return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
2041
+
2042
+ if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
2043
+ if frame_id:
2044
+ start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
2045
+ else:
2046
+ start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
2047
+
2048
+ stream_time_str = self._format_timestamp_for_video(start_time)
2049
+
2050
+
2051
+ return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
2052
+ else:
2053
+ stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
2054
+ if stream_time_str:
2055
+ try:
2056
+ timestamp_str = stream_time_str.replace(" UTC", "")
2057
+ dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
2058
+ timestamp = dt.replace(tzinfo=timezone.utc).timestamp()
2059
+ return self._format_timestamp_for_stream(timestamp)
2060
+ except:
2061
+ return self._format_timestamp_for_stream(time.time())
2062
+ else:
2063
+ return self._format_timestamp_for_stream(time.time())
2064
+
2065
+ def _get_start_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False) -> str:
2066
+ """Get formatted start timestamp for 'TOTAL SINCE' based on stream type."""
2067
+ if not stream_info:
2068
+ return "00:00:00"
2069
+
2070
+ if precision:
2071
+ if self.start_timer is None:
2072
+ candidate = stream_info.get("input_settings", {}).get("stream_time")
2073
+ if not candidate or candidate == "NA":
2074
+ candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
2075
+ self.start_timer = candidate
2076
+ return self._format_timestamp(self.start_timer)
2077
+ elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
2078
+ candidate = stream_info.get("input_settings", {}).get("stream_time")
2079
+ if not candidate or candidate == "NA":
2080
+ candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
2081
+ self.start_timer = candidate
2082
+ return self._format_timestamp(self.start_timer)
2083
+ else:
2084
+ return self._format_timestamp(self.start_timer)
2085
+
2086
+ if self.start_timer is None:
2087
+ # Prefer direct input_settings.stream_time if available and not NA
2088
+ candidate = stream_info.get("input_settings", {}).get("stream_time")
2089
+ if not candidate or candidate == "NA":
2090
+ # Fallback to nested stream_info.stream_time used by current timestamp path
2091
+ stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
2092
+ if stream_time_str:
2093
+ try:
2094
+ timestamp_str = stream_time_str.replace(" UTC", "")
2095
+ dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
2096
+ self._tracking_start_time = dt.replace(tzinfo=timezone.utc).timestamp()
2097
+ candidate = datetime.fromtimestamp(self._tracking_start_time, timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
2098
+ except:
2099
+ candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
2100
+ else:
2101
+ candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
2102
+ self.start_timer = candidate
2103
+ return self._format_timestamp(self.start_timer)
2104
+ elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
2105
+ candidate = stream_info.get("input_settings", {}).get("stream_time")
2106
+ if not candidate or candidate == "NA":
2107
+ stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
2108
+ if stream_time_str:
2109
+ try:
2110
+ timestamp_str = stream_time_str.replace(" UTC", "")
2111
+ dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
2112
+ ts = dt.replace(tzinfo=timezone.utc).timestamp()
2113
+ candidate = datetime.fromtimestamp(ts, timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
2114
+ except:
2115
+ candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
2116
+ else:
2117
+ candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
2118
+ self.start_timer = candidate
2119
+ return self._format_timestamp(self.start_timer)
2120
+
2121
+ else:
2122
+ if self.start_timer is not None and self.start_timer != "NA":
2123
+ return self._format_timestamp(self.start_timer)
2124
+
2125
+ if self._tracking_start_time is None:
2126
+ stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
2127
+ if stream_time_str:
2128
+ try:
2129
+ timestamp_str = stream_time_str.replace(" UTC", "")
2130
+ dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
2131
+ self._tracking_start_time = dt.replace(tzinfo=timezone.utc).timestamp()
2132
+ except:
2133
+ self._tracking_start_time = time.time()
2134
+ else:
2135
+ self._tracking_start_time = time.time()
2136
+
2137
+ dt = datetime.fromtimestamp(self._tracking_start_time, tz=timezone.utc)
2138
+ dt = dt.replace(minute=0, second=0, microsecond=0)
2139
+ return dt.strftime('%Y:%m:%d %H:%M:%S')
2140
+
2141
+
2142
+ def _compute_iou(self, box1: Any, box2: Any) -> float:
2143
+ """Compute IoU between two bounding boxes which may be dicts or lists."""
2144
+
2145
+ def _bbox_to_list(bbox):
2146
+ if bbox is None:
2147
+ return []
2148
+ if isinstance(bbox, list):
2149
+ return bbox[:4] if len(bbox) >= 4 else []
2150
+ if isinstance(bbox, dict):
2151
+ if "xmin" in bbox:
2152
+ return [bbox["xmin"], bbox["ymin"], bbox["xmax"], bbox["ymax"]]
2153
+ if "x1" in bbox:
2154
+ return [bbox["x1"], bbox["y1"], bbox["x2"], bbox["y2"]]
2155
+ values = [v for v in bbox.values() if isinstance(v, (int, float))]
2156
+ return values[:4] if len(values) >= 4 else []
2157
+ return []
2158
+
2159
+ l1 = _bbox_to_list(box1)
2160
+ l2 = _bbox_to_list(box2)
2161
+ if len(l1) < 4 or len(l2) < 4:
2162
+ return 0.0
2163
+ x1_min, y1_min, x1_max, y1_max = l1
2164
+ x2_min, y2_min, x2_max, y2_max = l2
2165
+
2166
+ x1_min, x1_max = min(x1_min, x1_max), max(x1_min, x1_max)
2167
+ y1_min, y1_max = min(y1_min, y1_max), max(y1_min, y1_max)
2168
+ x2_min, x2_max = min(x2_min, x2_max), max(x2_min, x2_max)
2169
+ y2_min, y2_max = min(y2_min, y2_max), max(y2_min, y2_max)
2170
+
2171
+ inter_x_min = max(x1_min, x2_min)
2172
+ inter_y_min = max(y1_min, y2_min)
2173
+ inter_x_max = min(x1_max, x2_max)
2174
+ inter_y_max = min(y1_max, y2_max)
2175
+
2176
+ inter_w = max(0.0, inter_x_max - inter_x_min)
2177
+ inter_h = max(0.0, inter_y_max - inter_y_min)
2178
+ inter_area = inter_w * inter_h
2179
+
2180
+ area1 = (x1_max - x1_min) * (y1_max - y1_min)
2181
+ area2 = (x2_max - x2_min) * (y2_max - y2_min)
2182
+ union_area = area1 + area2 - inter_area
2183
+
2184
+ return (inter_area / union_area) if union_area > 0 else 0.0
2185
+
2186
+ def _merge_or_register_track(self, raw_id: Any, bbox: Any) -> Any:
2187
+ """Return a stable canonical ID for a raw tracker ID."""
2188
+ if raw_id is None or bbox is None:
2189
+ return raw_id
2190
+
2191
+ now = time.time()
2192
+
2193
+ if raw_id in self._track_aliases:
2194
+ canonical_id = self._track_aliases[raw_id]
2195
+ track_info = self._canonical_tracks.get(canonical_id)
2196
+ if track_info is not None:
2197
+ track_info["last_bbox"] = bbox
2198
+ track_info["last_update"] = now
2199
+ track_info["raw_ids"].add(raw_id)
2200
+ return canonical_id
2201
+
2202
+ for canonical_id, info in self._canonical_tracks.items():
2203
+ if now - info["last_update"] > self._track_merge_time_window:
2204
+ continue
2205
+ iou = self._compute_iou(bbox, info["last_bbox"])
2206
+ if iou >= self._track_merge_iou_threshold:
2207
+ self._track_aliases[raw_id] = canonical_id
2208
+ info["last_bbox"] = bbox
2209
+ info["last_update"] = now
2210
+ info["raw_ids"].add(raw_id)
2211
+ return canonical_id
2212
+
2213
+ canonical_id = raw_id
2214
+ self._track_aliases[raw_id] = canonical_id
2215
+ self._canonical_tracks[canonical_id] = {
2216
+ "last_bbox": bbox,
2217
+ "last_update": now,
2218
+ "raw_ids": {raw_id},
2219
+ }
2220
+ return canonical_id
2221
+
2222
+ def __del__(self):
2223
+ """Cleanup when object is destroyed"""
2224
+ try:
2225
+ if hasattr(self, "people_activity_logging") and self.people_activity_logging:
2226
+ self.people_activity_logging.stop_background_processing()
2227
+ except:
2228
+ pass
2229
+
2230
+ try:
2231
+ if hasattr(self, "embedding_manager") and self.embedding_manager:
2232
+ self.embedding_manager.stop_background_refresh()
2233
+ except:
2234
+ pass