Refactor: done phase 5
This commit is contained in:
		
							parent
							
								
									5176f99ba7
								
							
						
					
					
						commit
						476f19cabe
					
				
					 3 changed files with 740 additions and 109 deletions
				
			
		| 
						 | 
				
			
			@ -520,6 +520,58 @@ class BranchProcessor:
 | 
			
		|||
            else:
 | 
			
		||||
                logger.warning(f"[NO RESULTS] {branch_id}: No detections found")
 | 
			
		||||
 | 
			
		||||
            # Execute branch actions if this branch found valid detections
 | 
			
		||||
            actions_executed = []
 | 
			
		||||
            branch_actions = getattr(branch_config, 'actions', [])
 | 
			
		||||
            if branch_actions and branch_detections:
 | 
			
		||||
                logger.info(f"[BRANCH ACTIONS] {branch_id}: Executing {len(branch_actions)} actions")
 | 
			
		||||
 | 
			
		||||
                # Create detected_regions from THIS branch's detections for actions
 | 
			
		||||
                branch_detected_regions = {}
 | 
			
		||||
                for detection in branch_detections:
 | 
			
		||||
                    branch_detected_regions[detection['class_name']] = {
 | 
			
		||||
                        'bbox': detection['bbox'],
 | 
			
		||||
                        'confidence': detection['confidence']
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                for action in branch_actions:
 | 
			
		||||
                    try:
 | 
			
		||||
                        action_type = action.type.value  # Access the enum value
 | 
			
		||||
                        logger.info(f"[ACTION EXECUTE] {branch_id}: Executing action '{action_type}'")
 | 
			
		||||
 | 
			
		||||
                        if action_type == 'redis_save_image':
 | 
			
		||||
                            action_result = self._execute_redis_save_image_sync(
 | 
			
		||||
                                action, input_frame, branch_detected_regions, detection_context
 | 
			
		||||
                            )
 | 
			
		||||
                        elif action_type == 'redis_publish':
 | 
			
		||||
                            action_result = self._execute_redis_publish_sync(
 | 
			
		||||
                                action, detection_context
 | 
			
		||||
                            )
 | 
			
		||||
                        else:
 | 
			
		||||
                            logger.warning(f"[ACTION UNKNOWN] {branch_id}: Unknown action type '{action_type}'")
 | 
			
		||||
                            action_result = {'status': 'error', 'message': f'Unknown action type: {action_type}'}
 | 
			
		||||
 | 
			
		||||
                        actions_executed.append({
 | 
			
		||||
                            'action_type': action_type,
 | 
			
		||||
                            'result': action_result
 | 
			
		||||
                        })
 | 
			
		||||
 | 
			
		||||
                        logger.info(f"[ACTION COMPLETE] {branch_id}: Action '{action_type}' result: {action_result.get('status')}")
 | 
			
		||||
 | 
			
		||||
                    except Exception as e:
 | 
			
		||||
                        action_type = getattr(action, 'type', None)
 | 
			
		||||
                        if action_type:
 | 
			
		||||
                            action_type = action_type.value if hasattr(action_type, 'value') else str(action_type)
 | 
			
		||||
                        logger.error(f"[ACTION ERROR] {branch_id}: Error executing action '{action_type}': {e}", exc_info=True)
 | 
			
		||||
                        actions_executed.append({
 | 
			
		||||
                            'action_type': action_type,
 | 
			
		||||
                            'result': {'status': 'error', 'message': str(e)}
 | 
			
		||||
                        })
 | 
			
		||||
 | 
			
		||||
            # Add actions executed to result
 | 
			
		||||
            if actions_executed:
 | 
			
		||||
                result['actions_executed'] = actions_executed
 | 
			
		||||
 | 
			
		||||
            # Handle nested branches ONLY if parent found valid detections
 | 
			
		||||
            nested_branches = getattr(branch_config, 'branches', [])
 | 
			
		||||
            if nested_branches:
 | 
			
		||||
| 
						 | 
				
			
			@ -566,6 +618,164 @@ class BranchProcessor:
 | 
			
		|||
 | 
			
		||||
        return result
 | 
			
		||||
 | 
			
		||||
    def _execute_redis_save_image_sync(self,
 | 
			
		||||
                                     action: Dict,
 | 
			
		||||
                                     frame: np.ndarray,
 | 
			
		||||
                                     detected_regions: Dict[str, Any],
 | 
			
		||||
                                     context: Dict[str, Any]) -> Dict[str, Any]:
 | 
			
		||||
        """Execute redis_save_image action synchronously."""
 | 
			
		||||
        if not self.redis_manager:
 | 
			
		||||
            return {'status': 'error', 'message': 'Redis not available'}
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            # Get image to save (cropped or full frame)
 | 
			
		||||
            image_to_save = frame
 | 
			
		||||
            region_name = action.params.get('region')
 | 
			
		||||
 | 
			
		||||
            bbox = None
 | 
			
		||||
            if region_name and region_name in detected_regions:
 | 
			
		||||
                # Crop the specified region
 | 
			
		||||
                bbox = detected_regions[region_name]['bbox']
 | 
			
		||||
            elif region_name and region_name.lower() == 'frontal' and 'front_rear' in detected_regions:
 | 
			
		||||
                # Special case: "frontal" region maps to "front_rear" detection
 | 
			
		||||
                bbox = detected_regions['front_rear']['bbox']
 | 
			
		||||
 | 
			
		||||
            if bbox is not None:
 | 
			
		||||
                x1, y1, x2, y2 = [int(coord) for coord in bbox]
 | 
			
		||||
                cropped = frame[y1:y2, x1:x2]
 | 
			
		||||
                if cropped.size > 0:
 | 
			
		||||
                    image_to_save = cropped
 | 
			
		||||
                    logger.debug(f"Cropped region '{region_name}' for redis_save_image")
 | 
			
		||||
                else:
 | 
			
		||||
                    logger.warning(f"Empty crop for region '{region_name}', using full frame")
 | 
			
		||||
 | 
			
		||||
            # Format key with context
 | 
			
		||||
            key = action.params['key'].format(**context)
 | 
			
		||||
 | 
			
		||||
            # Convert image to bytes
 | 
			
		||||
            import cv2
 | 
			
		||||
            image_format = action.params.get('format', 'jpeg')
 | 
			
		||||
            quality = action.params.get('quality', 90)
 | 
			
		||||
 | 
			
		||||
            if image_format.lower() == 'jpeg':
 | 
			
		||||
                encode_param = [cv2.IMWRITE_JPEG_QUALITY, quality]
 | 
			
		||||
                _, image_bytes = cv2.imencode('.jpg', image_to_save, encode_param)
 | 
			
		||||
            else:
 | 
			
		||||
                _, image_bytes = cv2.imencode('.png', image_to_save)
 | 
			
		||||
 | 
			
		||||
            # Save to Redis synchronously using a sync Redis client
 | 
			
		||||
            try:
 | 
			
		||||
                import redis
 | 
			
		||||
                import cv2
 | 
			
		||||
 | 
			
		||||
                # Create a synchronous Redis client with same connection details
 | 
			
		||||
                sync_redis = redis.Redis(
 | 
			
		||||
                    host=self.redis_manager.host,
 | 
			
		||||
                    port=self.redis_manager.port,
 | 
			
		||||
                    password=self.redis_manager.password,
 | 
			
		||||
                    db=self.redis_manager.db,
 | 
			
		||||
                    decode_responses=False,  # We're storing binary data
 | 
			
		||||
                    socket_timeout=self.redis_manager.socket_timeout,
 | 
			
		||||
                    socket_connect_timeout=self.redis_manager.socket_connect_timeout
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                # Encode the image
 | 
			
		||||
                if image_format.lower() == 'jpeg':
 | 
			
		||||
                    encode_param = [cv2.IMWRITE_JPEG_QUALITY, quality]
 | 
			
		||||
                    success, encoded_image = cv2.imencode('.jpg', image_to_save, encode_param)
 | 
			
		||||
                else:
 | 
			
		||||
                    success, encoded_image = cv2.imencode('.png', image_to_save)
 | 
			
		||||
 | 
			
		||||
                if not success:
 | 
			
		||||
                    return {'status': 'error', 'message': 'Failed to encode image'}
 | 
			
		||||
 | 
			
		||||
                # Save to Redis with expiration
 | 
			
		||||
                expire_seconds = action.params.get('expire_seconds', 600)
 | 
			
		||||
                result = sync_redis.setex(key, expire_seconds, encoded_image.tobytes())
 | 
			
		||||
 | 
			
		||||
                sync_redis.close()  # Clean up connection
 | 
			
		||||
 | 
			
		||||
                if result:
 | 
			
		||||
                    # Add image_key to context for subsequent actions
 | 
			
		||||
                    context['image_key'] = key
 | 
			
		||||
                    return {'status': 'success', 'key': key}
 | 
			
		||||
                else:
 | 
			
		||||
                    return {'status': 'error', 'message': 'Failed to save image to Redis'}
 | 
			
		||||
 | 
			
		||||
            except Exception as redis_error:
 | 
			
		||||
                logger.error(f"Error calling Redis from sync context: {redis_error}")
 | 
			
		||||
                return {'status': 'error', 'message': f'Redis operation failed: {redis_error}'}
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(f"Error in redis_save_image action: {e}", exc_info=True)
 | 
			
		||||
            return {'status': 'error', 'message': str(e)}
 | 
			
		||||
 | 
			
		||||
    def _execute_redis_publish_sync(self, action: Dict, context: Dict[str, Any]) -> Dict[str, Any]:
 | 
			
		||||
        """Execute redis_publish action synchronously."""
 | 
			
		||||
        if not self.redis_manager:
 | 
			
		||||
            return {'status': 'error', 'message': 'Redis not available'}
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            channel = action.params['channel']
 | 
			
		||||
            message_template = action.params['message']
 | 
			
		||||
 | 
			
		||||
            # Debug the message template
 | 
			
		||||
            logger.debug(f"Message template: {repr(message_template)}")
 | 
			
		||||
            logger.debug(f"Context keys: {list(context.keys())}")
 | 
			
		||||
 | 
			
		||||
            # Format message with context - handle JSON string formatting carefully
 | 
			
		||||
            # The message template contains JSON which causes issues with .format()
 | 
			
		||||
            # Use string replacement instead of format to avoid JSON brace conflicts
 | 
			
		||||
            try:
 | 
			
		||||
                # Ensure image_key is available for message formatting
 | 
			
		||||
                if 'image_key' not in context:
 | 
			
		||||
                    context['image_key'] = ''  # Default empty value if redis_save_image failed
 | 
			
		||||
 | 
			
		||||
                # Use string replacement to avoid JSON formatting issues
 | 
			
		||||
                message = message_template
 | 
			
		||||
                for key, value in context.items():
 | 
			
		||||
                    placeholder = '{' + key + '}'
 | 
			
		||||
                    message = message.replace(placeholder, str(value))
 | 
			
		||||
 | 
			
		||||
                logger.debug(f"Formatted message using replacement: {message}")
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                logger.error(f"Message formatting failed: {e}")
 | 
			
		||||
                logger.error(f"Template: {repr(message_template)}")
 | 
			
		||||
                logger.error(f"Context: {context}")
 | 
			
		||||
                return {'status': 'error', 'message': f'Message formatting failed: {e}'}
 | 
			
		||||
 | 
			
		||||
            # Publish message synchronously using a sync Redis client
 | 
			
		||||
            try:
 | 
			
		||||
                import redis
 | 
			
		||||
 | 
			
		||||
                # Create a synchronous Redis client with same connection details
 | 
			
		||||
                sync_redis = redis.Redis(
 | 
			
		||||
                    host=self.redis_manager.host,
 | 
			
		||||
                    port=self.redis_manager.port,
 | 
			
		||||
                    password=self.redis_manager.password,
 | 
			
		||||
                    db=self.redis_manager.db,
 | 
			
		||||
                    decode_responses=True,  # For publishing text messages
 | 
			
		||||
                    socket_timeout=self.redis_manager.socket_timeout,
 | 
			
		||||
                    socket_connect_timeout=self.redis_manager.socket_connect_timeout
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                # Publish message
 | 
			
		||||
                result = sync_redis.publish(channel, message)
 | 
			
		||||
                sync_redis.close()  # Clean up connection
 | 
			
		||||
 | 
			
		||||
                if result >= 0:  # Redis publish returns number of subscribers
 | 
			
		||||
                    return {'status': 'success', 'subscribers': result, 'channel': channel}
 | 
			
		||||
                else:
 | 
			
		||||
                    return {'status': 'error', 'message': 'Failed to publish message to Redis'}
 | 
			
		||||
 | 
			
		||||
            except Exception as redis_error:
 | 
			
		||||
                logger.error(f"Error calling Redis from sync context: {redis_error}")
 | 
			
		||||
                return {'status': 'error', 'message': f'Redis operation failed: {redis_error}'}
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(f"Error in redis_publish action: {e}", exc_info=True)
 | 
			
		||||
            return {'status': 'error', 'message': str(e)}
 | 
			
		||||
 | 
			
		||||
    def get_statistics(self) -> Dict[str, Any]:
 | 
			
		||||
        """Get branch processor statistics."""
 | 
			
		||||
        return {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@
 | 
			
		|||
Detection Pipeline Module.
 | 
			
		||||
Main detection pipeline orchestration that coordinates detection flow and execution.
 | 
			
		||||
"""
 | 
			
		||||
import asyncio
 | 
			
		||||
import logging
 | 
			
		||||
import time
 | 
			
		||||
import uuid
 | 
			
		||||
| 
						 | 
				
			
			@ -15,6 +16,7 @@ from ..models.pipeline import PipelineParser
 | 
			
		|||
from .branches import BranchProcessor
 | 
			
		||||
from ..storage.redis import RedisManager
 | 
			
		||||
from ..storage.database import DatabaseManager
 | 
			
		||||
from ..storage.license_plate import LicensePlateManager
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -42,6 +44,7 @@ class DetectionPipeline:
 | 
			
		|||
        self.branch_processor = BranchProcessor(model_manager)
 | 
			
		||||
        self.redis_manager = None
 | 
			
		||||
        self.db_manager = None
 | 
			
		||||
        self.license_plate_manager = None
 | 
			
		||||
 | 
			
		||||
        # Main detection model
 | 
			
		||||
        self.detection_model: Optional[YOLOWrapper] = None
 | 
			
		||||
| 
						 | 
				
			
			@ -53,6 +56,12 @@ class DetectionPipeline:
 | 
			
		|||
        # Pipeline configuration
 | 
			
		||||
        self.pipeline_config = pipeline_parser.pipeline_config
 | 
			
		||||
 | 
			
		||||
        # SessionId to subscriptionIdentifier mapping
 | 
			
		||||
        self.session_to_subscription = {}
 | 
			
		||||
 | 
			
		||||
        # SessionId to processing results mapping (for combining with license plate results)
 | 
			
		||||
        self.session_processing_results = {}
 | 
			
		||||
 | 
			
		||||
        # Statistics
 | 
			
		||||
        self.stats = {
 | 
			
		||||
            'detections_processed': 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -90,6 +99,15 @@ class DetectionPipeline:
 | 
			
		|||
                    logger.warning("Failed to create car_frontal_info table")
 | 
			
		||||
                logger.info("Database connection initialized")
 | 
			
		||||
 | 
			
		||||
            # Initialize license plate manager (using same Redis config as main Redis manager)
 | 
			
		||||
            if self.pipeline_parser.redis_config:
 | 
			
		||||
                self.license_plate_manager = LicensePlateManager(self.pipeline_parser.redis_config.__dict__)
 | 
			
		||||
                if not await self.license_plate_manager.initialize(self._on_license_plate_result):
 | 
			
		||||
                    logger.error("Failed to initialize license plate manager")
 | 
			
		||||
                    return False
 | 
			
		||||
                logger.info("License plate manager initialized")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            # Initialize main detection model
 | 
			
		||||
            if not await self._initialize_detection_model():
 | 
			
		||||
                logger.error("Failed to initialize detection model")
 | 
			
		||||
| 
						 | 
				
			
			@ -154,6 +172,193 @@ class DetectionPipeline:
 | 
			
		|||
            logger.error(f"Error initializing detection model: {e}", exc_info=True)
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
    async def _on_license_plate_result(self, session_id: str, license_data: Dict[str, Any]):
 | 
			
		||||
        """
 | 
			
		||||
        Callback for handling license plate results from LPR service.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            session_id: Session identifier
 | 
			
		||||
            license_data: License plate data including text and confidence
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            license_text = license_data.get('license_plate_text', '')
 | 
			
		||||
            confidence = license_data.get('confidence', 0.0)
 | 
			
		||||
 | 
			
		||||
            logger.info(f"[LICENSE PLATE CALLBACK] Session {session_id}: "
 | 
			
		||||
                       f"text='{license_text}', confidence={confidence:.3f}")
 | 
			
		||||
 | 
			
		||||
            # Find matching subscriptionIdentifier for this sessionId
 | 
			
		||||
            subscription_id = self.session_to_subscription.get(session_id)
 | 
			
		||||
 | 
			
		||||
            if not subscription_id:
 | 
			
		||||
                logger.warning(f"[LICENSE PLATE] No subscription found for sessionId '{session_id}' (type: {type(session_id)}), cannot send imageDetection")
 | 
			
		||||
                logger.warning(f"[LICENSE PLATE DEBUG] Current session mappings: {dict(self.session_to_subscription)}")
 | 
			
		||||
 | 
			
		||||
                # Try to find by type conversion in case of type mismatch
 | 
			
		||||
                # Try as integer if session_id is string
 | 
			
		||||
                if isinstance(session_id, str) and session_id.isdigit():
 | 
			
		||||
                    session_id_int = int(session_id)
 | 
			
		||||
                    subscription_id = self.session_to_subscription.get(session_id_int)
 | 
			
		||||
                    if subscription_id:
 | 
			
		||||
                        logger.info(f"[LICENSE PLATE] Found subscription using int conversion: '{session_id}' -> {session_id_int} -> '{subscription_id}'")
 | 
			
		||||
                    else:
 | 
			
		||||
                        logger.error(f"[LICENSE PLATE] Failed to find subscription with int conversion")
 | 
			
		||||
                        return
 | 
			
		||||
                # Try as string if session_id is integer
 | 
			
		||||
                elif isinstance(session_id, int):
 | 
			
		||||
                    session_id_str = str(session_id)
 | 
			
		||||
                    subscription_id = self.session_to_subscription.get(session_id_str)
 | 
			
		||||
                    if subscription_id:
 | 
			
		||||
                        logger.info(f"[LICENSE PLATE] Found subscription using string conversion: {session_id} -> '{session_id_str}' -> '{subscription_id}'")
 | 
			
		||||
                    else:
 | 
			
		||||
                        logger.error(f"[LICENSE PLATE] Failed to find subscription with string conversion")
 | 
			
		||||
                        return
 | 
			
		||||
                else:
 | 
			
		||||
                    logger.error(f"[LICENSE PLATE] Failed to find subscription with any type conversion")
 | 
			
		||||
                    return
 | 
			
		||||
 | 
			
		||||
            # Send imageDetection message with license plate data combined with processing results
 | 
			
		||||
            await self._send_license_plate_message(subscription_id, license_text, confidence, session_id)
 | 
			
		||||
 | 
			
		||||
            # Update database with license plate information if database manager is available
 | 
			
		||||
            if self.db_manager and license_text:
 | 
			
		||||
                success = self.db_manager.execute_update(
 | 
			
		||||
                    table='car_frontal_info',
 | 
			
		||||
                    key_field='session_id',
 | 
			
		||||
                    key_value=session_id,
 | 
			
		||||
                    fields={
 | 
			
		||||
                        'license_character': license_text,
 | 
			
		||||
                        'license_type': 'LPR_detected'  # Mark as detected by LPR service
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
                if success:
 | 
			
		||||
                    logger.info(f"[LICENSE PLATE] Updated database for session {session_id}")
 | 
			
		||||
                else:
 | 
			
		||||
                    logger.warning(f"[LICENSE PLATE] Failed to update database for session {session_id}")
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(f"Error in license plate result callback: {e}", exc_info=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    async def _send_license_plate_message(self, subscription_id: str, license_text: str, confidence: float, session_id: str = None):
 | 
			
		||||
        """
 | 
			
		||||
        Send imageDetection message with license plate data plus any available processing results.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            subscription_id: Subscription identifier to send message to
 | 
			
		||||
            license_text: License plate text
 | 
			
		||||
            confidence: License plate confidence score
 | 
			
		||||
            session_id: Session identifier for looking up processing results
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            if not self.message_sender:
 | 
			
		||||
                logger.warning("No message sender configured, cannot send imageDetection")
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            # Import here to avoid circular imports
 | 
			
		||||
            from ..communication.models import ImageDetectionMessage, DetectionData
 | 
			
		||||
 | 
			
		||||
            # Get processing results for this session from stored results
 | 
			
		||||
            car_brand = None
 | 
			
		||||
            body_type = None
 | 
			
		||||
 | 
			
		||||
            # Find session_id from session mappings (we need session_id as key)
 | 
			
		||||
            session_id_for_lookup = None
 | 
			
		||||
 | 
			
		||||
            # Try direct lookup first (if session_id is already the right type)
 | 
			
		||||
            if session_id in self.session_processing_results:
 | 
			
		||||
                session_id_for_lookup = session_id
 | 
			
		||||
            else:
 | 
			
		||||
                # Try to find by type conversion
 | 
			
		||||
                for stored_session_id in self.session_processing_results.keys():
 | 
			
		||||
                    if str(stored_session_id) == str(session_id):
 | 
			
		||||
                        session_id_for_lookup = stored_session_id
 | 
			
		||||
                        break
 | 
			
		||||
 | 
			
		||||
            if session_id_for_lookup and session_id_for_lookup in self.session_processing_results:
 | 
			
		||||
                branch_results = self.session_processing_results[session_id_for_lookup]
 | 
			
		||||
                logger.info(f"[LICENSE PLATE] Retrieved processing results for session {session_id_for_lookup}")
 | 
			
		||||
 | 
			
		||||
                if 'car_brand_cls_v2' in branch_results:
 | 
			
		||||
                    brand_result = branch_results['car_brand_cls_v2'].get('result', {})
 | 
			
		||||
                    car_brand = brand_result.get('brand')
 | 
			
		||||
                if 'car_bodytype_cls_v1' in branch_results:
 | 
			
		||||
                    bodytype_result = branch_results['car_bodytype_cls_v1'].get('result', {})
 | 
			
		||||
                    body_type = bodytype_result.get('body_type')
 | 
			
		||||
 | 
			
		||||
                # Clean up stored results after use
 | 
			
		||||
                del self.session_processing_results[session_id_for_lookup]
 | 
			
		||||
                logger.debug(f"[LICENSE PLATE] Cleaned up stored results for session {session_id_for_lookup}")
 | 
			
		||||
            else:
 | 
			
		||||
                logger.warning(f"[LICENSE PLATE] No processing results found for session {session_id}")
 | 
			
		||||
 | 
			
		||||
            # Create detection data with combined information
 | 
			
		||||
            detection_data_obj = DetectionData(
 | 
			
		||||
                detection={
 | 
			
		||||
                    "carBrand": car_brand,
 | 
			
		||||
                    "carModel": None,
 | 
			
		||||
                    "bodyType": body_type,
 | 
			
		||||
                    "licensePlateText": license_text,
 | 
			
		||||
                    "licensePlateConfidence": confidence
 | 
			
		||||
                },
 | 
			
		||||
                modelId=52,  # Default model ID
 | 
			
		||||
                modelName="yolo11m"  # Default model name
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # Create imageDetection message
 | 
			
		||||
            detection_message = ImageDetectionMessage(
 | 
			
		||||
                subscriptionIdentifier=subscription_id,
 | 
			
		||||
                data=detection_data_obj
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # Send message
 | 
			
		||||
            await self.message_sender(detection_message)
 | 
			
		||||
            logger.info(f"[COMBINED MESSAGE] Sent imageDetection with brand='{car_brand}', bodyType='{body_type}', license='{license_text}' to '{subscription_id}'")
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(f"Error sending license plate imageDetection message: {e}", exc_info=True)
 | 
			
		||||
 | 
			
		||||
    async def _send_initial_detection_message(self, subscription_id: str):
 | 
			
		||||
        """
 | 
			
		||||
        Send initial imageDetection message when vehicle is first detected.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            subscription_id: Subscription identifier to send message to
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            if not self.message_sender:
 | 
			
		||||
                logger.warning("No message sender configured, cannot send imageDetection")
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            # Import here to avoid circular imports
 | 
			
		||||
            from ..communication.models import ImageDetectionMessage, DetectionData
 | 
			
		||||
 | 
			
		||||
            # Create detection data with all fields as None (vehicle just detected, no classification yet)
 | 
			
		||||
            detection_data_obj = DetectionData(
 | 
			
		||||
                detection={
 | 
			
		||||
                    "carBrand": None,
 | 
			
		||||
                    "carModel": None,
 | 
			
		||||
                    "bodyType": None,
 | 
			
		||||
                    "licensePlateText": None,
 | 
			
		||||
                    "licensePlateConfidence": None
 | 
			
		||||
                },
 | 
			
		||||
                modelId=52,  # Default model ID
 | 
			
		||||
                modelName="yolo11m"  # Default model name
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # Create imageDetection message
 | 
			
		||||
            detection_message = ImageDetectionMessage(
 | 
			
		||||
                subscriptionIdentifier=subscription_id,
 | 
			
		||||
                data=detection_data_obj
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # Send message
 | 
			
		||||
            await self.message_sender(detection_message)
 | 
			
		||||
            logger.info(f"[INITIAL DETECTION] Sent imageDetection for vehicle detection to '{subscription_id}'")
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(f"Error sending initial detection imageDetection message: {e}", exc_info=True)
 | 
			
		||||
 | 
			
		||||
    async def execute_detection_phase(self,
 | 
			
		||||
                                    frame: np.ndarray,
 | 
			
		||||
                                    display_id: str,
 | 
			
		||||
| 
						 | 
				
			
			@ -249,21 +454,20 @@ class DetectionPipeline:
 | 
			
		|||
 | 
			
		||||
            result['detections'] = valid_detections
 | 
			
		||||
 | 
			
		||||
            # If we have valid detections, send imageDetection message with empty detection
 | 
			
		||||
            # If we have valid detections, create session and send initial imageDetection
 | 
			
		||||
            if valid_detections:
 | 
			
		||||
                logger.info(f"Found {len(valid_detections)} valid detections, sending imageDetection message")
 | 
			
		||||
                logger.info(f"Found {len(valid_detections)} valid detections, storing session mapping")
 | 
			
		||||
 | 
			
		||||
                # Send imageDetection with empty detection data
 | 
			
		||||
                message_sent = await self._send_image_detection_message(
 | 
			
		||||
                    subscription_id=subscription_id,
 | 
			
		||||
                    detection_context=detection_context
 | 
			
		||||
                )
 | 
			
		||||
                result['message_sent'] = message_sent
 | 
			
		||||
                # Store mapping from display_id to subscriptionIdentifier (for detection phase)
 | 
			
		||||
                # Note: We'll store session_id mapping later in processing phase
 | 
			
		||||
                self.session_to_subscription[display_id] = subscription_id
 | 
			
		||||
                logger.info(f"[SESSION MAPPING] Stored mapping: displayId '{display_id}' -> subscriptionIdentifier '{subscription_id}'")
 | 
			
		||||
 | 
			
		||||
                if message_sent:
 | 
			
		||||
                    logger.info(f"Detection phase completed - imageDetection message sent for {display_id}")
 | 
			
		||||
                else:
 | 
			
		||||
                    logger.warning(f"Failed to send imageDetection message for {display_id}")
 | 
			
		||||
                # Send initial imageDetection message with empty detection data
 | 
			
		||||
                await self._send_initial_detection_message(subscription_id)
 | 
			
		||||
 | 
			
		||||
                logger.info(f"Detection phase completed - {len(valid_detections)} detections found for {display_id}")
 | 
			
		||||
                result['message_sent'] = True
 | 
			
		||||
            else:
 | 
			
		||||
                logger.debug("No valid detections found in detection phase")
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -341,6 +545,11 @@ class DetectionPipeline:
 | 
			
		|||
                                'confidence': confidence
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
            # Store session mapping for license plate callback
 | 
			
		||||
            if session_id:
 | 
			
		||||
                self.session_to_subscription[session_id] = subscription_id
 | 
			
		||||
                logger.info(f"[SESSION MAPPING] Stored mapping: sessionId '{session_id}' -> subscriptionIdentifier '{subscription_id}'")
 | 
			
		||||
 | 
			
		||||
            # Initialize database record with session_id
 | 
			
		||||
            if session_id and self.db_manager:
 | 
			
		||||
                success = self.db_manager.insert_initial_detection(
 | 
			
		||||
| 
						 | 
				
			
			@ -391,6 +600,11 @@ class DetectionPipeline:
 | 
			
		|||
                )
 | 
			
		||||
                result['actions_executed'].extend(executed_parallel_actions)
 | 
			
		||||
 | 
			
		||||
            # Store processing results for later combination with license plate data
 | 
			
		||||
            if result['branch_results'] and session_id:
 | 
			
		||||
                self.session_processing_results[session_id] = result['branch_results']
 | 
			
		||||
                logger.info(f"[PROCESSING RESULTS] Stored results for session {session_id} for later combination")
 | 
			
		||||
 | 
			
		||||
            logger.info(f"Processing phase completed for session {session_id}: "
 | 
			
		||||
                       f"{len(result['branch_results'])} branches, {len(result['actions_executed'])} actions")
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -402,57 +616,6 @@ class DetectionPipeline:
 | 
			
		|||
        result['processing_time'] = time.time() - start_time
 | 
			
		||||
        return result
 | 
			
		||||
 | 
			
		||||
    async def _send_image_detection_message(self,
 | 
			
		||||
                                          subscription_id: str,
 | 
			
		||||
                                          detection_context: Dict[str, Any]) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
        Send imageDetection message with empty detection data to backend.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            subscription_id: Subscription identifier
 | 
			
		||||
            detection_context: Detection context data
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            True if message sent successfully, False otherwise
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            if not self.message_sender:
 | 
			
		||||
                logger.warning("No message sender available for imageDetection")
 | 
			
		||||
                return False
 | 
			
		||||
 | 
			
		||||
            # Import here to avoid circular imports
 | 
			
		||||
            from ..communication.messages import create_image_detection
 | 
			
		||||
 | 
			
		||||
            # Create empty detection data as specified
 | 
			
		||||
            detection_data = {}
 | 
			
		||||
 | 
			
		||||
            # Get model info from pipeline configuration
 | 
			
		||||
            model_id = 52  # Default model ID
 | 
			
		||||
            model_name = "yolo11m"  # Default
 | 
			
		||||
 | 
			
		||||
            if self.pipeline_config:
 | 
			
		||||
                model_name = getattr(self.pipeline_config, 'model_id', 'yolo11m')
 | 
			
		||||
                # Try to extract numeric model ID from pipeline context, fallback to default
 | 
			
		||||
                if hasattr(self.pipeline_config, 'model_id'):
 | 
			
		||||
                    # For now, use default model ID since pipeline config stores string identifiers
 | 
			
		||||
                    model_id = 52
 | 
			
		||||
 | 
			
		||||
            # Create imageDetection message
 | 
			
		||||
            detection_message = create_image_detection(
 | 
			
		||||
                subscription_identifier=subscription_id,
 | 
			
		||||
                detection_data=detection_data,
 | 
			
		||||
                model_id=model_id,
 | 
			
		||||
                model_name=model_name
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # Send to backend via WebSocket
 | 
			
		||||
            await self.message_sender(detection_message)
 | 
			
		||||
            logger.info(f"[DETECTION PHASE] Sent imageDetection with empty detection: {detection_data}")
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(f"Error sending imageDetection message: {e}", exc_info=True)
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
    async def execute_detection(self,
 | 
			
		||||
                              frame: np.ndarray,
 | 
			
		||||
| 
						 | 
				
			
			@ -697,9 +860,9 @@ class DetectionPipeline:
 | 
			
		|||
                if action_type == 'postgresql_update_combined':
 | 
			
		||||
                    result = await self._execute_postgresql_update_combined(action, context)
 | 
			
		||||
 | 
			
		||||
                    # Send imageDetection message with actual processing results after database update
 | 
			
		||||
                    # Update session state with processing results after database update
 | 
			
		||||
                    if result.get('status') == 'success':
 | 
			
		||||
                        await self._send_processing_results_message(context)
 | 
			
		||||
                        await self._update_session_with_processing_results(context)
 | 
			
		||||
                else:
 | 
			
		||||
                    logger.warning(f"Unknown parallel action type: {action_type}")
 | 
			
		||||
                    result = {'status': 'error', 'message': f'Unknown action type: {action_type}'}
 | 
			
		||||
| 
						 | 
				
			
			@ -889,76 +1052,49 @@ class DetectionPipeline:
 | 
			
		|||
            logger.error(f"Error resolving field template {template}: {e}")
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
    async def _send_processing_results_message(self, context: Dict[str, Any]):
 | 
			
		||||
    async def _update_session_with_processing_results(self, context: Dict[str, Any]):
 | 
			
		||||
        """
 | 
			
		||||
        Send imageDetection message with actual processing results after database update.
 | 
			
		||||
        Update session state with processing results from branch execution.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            context: Detection context containing branch results and subscription info
 | 
			
		||||
            context: Detection context containing branch results and session info
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            branch_results = context.get('branch_results', {})
 | 
			
		||||
            session_id = context.get('session_id', '')
 | 
			
		||||
            subscription_id = context.get('subscription_id', '')
 | 
			
		||||
 | 
			
		||||
            # Extract detection results from branch results
 | 
			
		||||
            detection_data = {
 | 
			
		||||
                "carBrand": None,
 | 
			
		||||
                "carModel": None,
 | 
			
		||||
                "bodyType": None,
 | 
			
		||||
                "licensePlateText": None,
 | 
			
		||||
                "licensePlateConfidence": None
 | 
			
		||||
            }
 | 
			
		||||
            if not session_id:
 | 
			
		||||
                logger.warning("No session_id in context for processing results")
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            # Extract car brand from car_brand_cls_v2 results
 | 
			
		||||
            car_brand = None
 | 
			
		||||
            if 'car_brand_cls_v2' in branch_results:
 | 
			
		||||
                brand_result = branch_results['car_brand_cls_v2'].get('result', {})
 | 
			
		||||
                detection_data["carBrand"] = brand_result.get('brand')
 | 
			
		||||
                car_brand = brand_result.get('brand')
 | 
			
		||||
 | 
			
		||||
            # Extract body type from car_bodytype_cls_v1 results
 | 
			
		||||
            body_type = None
 | 
			
		||||
            if 'car_bodytype_cls_v1' in branch_results:
 | 
			
		||||
                bodytype_result = branch_results['car_bodytype_cls_v1'].get('result', {})
 | 
			
		||||
                detection_data["bodyType"] = bodytype_result.get('body_type')
 | 
			
		||||
                body_type = bodytype_result.get('body_type')
 | 
			
		||||
 | 
			
		||||
            # Create detection message
 | 
			
		||||
            subscription_id = context.get('subscription_id', '')
 | 
			
		||||
            # Get the actual numeric model ID from context
 | 
			
		||||
            model_id_value = context.get('model_id', 52)
 | 
			
		||||
            if isinstance(model_id_value, str):
 | 
			
		||||
                try:
 | 
			
		||||
                    model_id_value = int(model_id_value)
 | 
			
		||||
                except (ValueError, TypeError):
 | 
			
		||||
                    model_id_value = 52
 | 
			
		||||
            model_name = str(getattr(self.pipeline_config, 'model_id', 'unknown'))
 | 
			
		||||
 | 
			
		||||
            logger.debug(f"Creating DetectionData with modelId={model_id_value}, modelName='{model_name}'")
 | 
			
		||||
 | 
			
		||||
            from core.communication.models import ImageDetectionMessage, DetectionData
 | 
			
		||||
            detection_data_obj = DetectionData(
 | 
			
		||||
                detection=detection_data,
 | 
			
		||||
                modelId=model_id_value,
 | 
			
		||||
                modelName=model_name
 | 
			
		||||
            )
 | 
			
		||||
            detection_message = ImageDetectionMessage(
 | 
			
		||||
                subscriptionIdentifier=subscription_id,
 | 
			
		||||
                data=detection_data_obj
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # Send to backend via WebSocket
 | 
			
		||||
            if self.message_sender:
 | 
			
		||||
                await self.message_sender(detection_message)
 | 
			
		||||
                logger.info(f"[RESULTS] Sent imageDetection with processing results: {detection_data}")
 | 
			
		||||
            else:
 | 
			
		||||
                logger.warning("No message sender available for processing results")
 | 
			
		||||
            logger.info(f"[PROCESSING RESULTS] Completed for session {session_id}: "
 | 
			
		||||
                       f"brand={car_brand}, bodyType={body_type}")
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(f"Error sending processing results message: {e}", exc_info=True)
 | 
			
		||||
            logger.error(f"Error updating session with processing results: {e}", exc_info=True)
 | 
			
		||||
 | 
			
		||||
    def get_statistics(self) -> Dict[str, Any]:
 | 
			
		||||
        """Get detection pipeline statistics."""
 | 
			
		||||
        branch_stats = self.branch_processor.get_statistics() if self.branch_processor else {}
 | 
			
		||||
        license_stats = self.license_plate_manager.get_statistics() if self.license_plate_manager else {}
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            'pipeline': self.stats,
 | 
			
		||||
            'branches': branch_stats,
 | 
			
		||||
            'license_plate': license_stats,
 | 
			
		||||
            'redis_available': self.redis_manager is not None,
 | 
			
		||||
            'database_available': self.db_manager is not None,
 | 
			
		||||
            'detection_model_loaded': self.detection_model is not None
 | 
			
		||||
| 
						 | 
				
			
			@ -978,4 +1114,7 @@ class DetectionPipeline:
 | 
			
		|||
        if self.branch_processor:
 | 
			
		||||
            self.branch_processor.cleanup()
 | 
			
		||||
 | 
			
		||||
        if self.license_plate_manager:
 | 
			
		||||
            asyncio.create_task(self.license_plate_manager.close())
 | 
			
		||||
 | 
			
		||||
        logger.info("Detection pipeline cleaned up")
 | 
			
		||||
							
								
								
									
										282
									
								
								core/storage/license_plate.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										282
									
								
								core/storage/license_plate.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,282 @@
 | 
			
		|||
"""
 | 
			
		||||
License Plate Manager Module.
 | 
			
		||||
Handles Redis subscription to license plate results from LPR service.
 | 
			
		||||
"""
 | 
			
		||||
import logging
 | 
			
		||||
import json
 | 
			
		||||
import asyncio
 | 
			
		||||
from typing import Dict, Optional, Any, Callable
 | 
			
		||||
import redis.asyncio as redis
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LicensePlateManager:
 | 
			
		||||
    """
 | 
			
		||||
    Manages license plate result subscription from Redis channel.
 | 
			
		||||
    Subscribes to 'license_results' channel for license plate data from LPR service.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(self, redis_config: Dict[str, Any]):
 | 
			
		||||
        """
 | 
			
		||||
        Initialize license plate manager with Redis configuration.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            redis_config: Redis configuration dictionary
 | 
			
		||||
        """
 | 
			
		||||
        self.config = redis_config
 | 
			
		||||
        self.redis_client: Optional[redis.Redis] = None
 | 
			
		||||
        self.pubsub = None
 | 
			
		||||
        self.subscription_task = None
 | 
			
		||||
        self.callback = None
 | 
			
		||||
 | 
			
		||||
        # Connection parameters
 | 
			
		||||
        self.host = redis_config.get('host', 'localhost')
 | 
			
		||||
        self.port = redis_config.get('port', 6379)
 | 
			
		||||
        self.password = redis_config.get('password')
 | 
			
		||||
        self.db = redis_config.get('db', 0)
 | 
			
		||||
 | 
			
		||||
        # License plate data cache - store recent results by session_id
 | 
			
		||||
        self.license_plate_cache: Dict[str, Dict[str, Any]] = {}
 | 
			
		||||
        self.cache_ttl = 300  # 5 minutes TTL for cached results
 | 
			
		||||
 | 
			
		||||
        logger.info(f"LicensePlateManager initialized for {self.host}:{self.port}")
 | 
			
		||||
 | 
			
		||||
    async def initialize(self, callback: Optional[Callable] = None) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
        Initialize Redis connection and start subscription to license_results channel.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            callback: Optional callback function for processing license plate results
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            True if successful, False otherwise
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            # Create Redis connection
 | 
			
		||||
            self.redis_client = redis.Redis(
 | 
			
		||||
                host=self.host,
 | 
			
		||||
                port=self.port,
 | 
			
		||||
                password=self.password,
 | 
			
		||||
                db=self.db,
 | 
			
		||||
                decode_responses=True
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # Test connection
 | 
			
		||||
            await self.redis_client.ping()
 | 
			
		||||
            logger.info(f"Connected to Redis for license plate subscription")
 | 
			
		||||
 | 
			
		||||
            # Set callback
 | 
			
		||||
            self.callback = callback
 | 
			
		||||
 | 
			
		||||
            # Start subscription
 | 
			
		||||
            await self._start_subscription()
 | 
			
		||||
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(f"Failed to initialize license plate manager: {e}", exc_info=True)
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
    async def _start_subscription(self):
 | 
			
		||||
        """Start Redis subscription to license_results channel."""
 | 
			
		||||
        try:
 | 
			
		||||
            if not self.redis_client:
 | 
			
		||||
                logger.error("Redis client not initialized")
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            # Create pubsub and subscribe
 | 
			
		||||
            self.pubsub = self.redis_client.pubsub()
 | 
			
		||||
            await self.pubsub.subscribe('license_results')
 | 
			
		||||
 | 
			
		||||
            logger.info("Subscribed to Redis channel: license_results")
 | 
			
		||||
 | 
			
		||||
            # Start listening task
 | 
			
		||||
            self.subscription_task = asyncio.create_task(self._listen_for_messages())
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(f"Error starting license plate subscription: {e}", exc_info=True)
 | 
			
		||||
 | 
			
		||||
    async def _listen_for_messages(self):
 | 
			
		||||
        """Listen for messages on the license_results channel."""
 | 
			
		||||
        try:
 | 
			
		||||
            if not self.pubsub:
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            async for message in self.pubsub.listen():
 | 
			
		||||
                if message['type'] == 'message':
 | 
			
		||||
                    try:
 | 
			
		||||
                        # Log the raw message from Redis channel
 | 
			
		||||
                        logger.info(f"[LICENSE PLATE RAW] Received from 'license_results' channel: {message['data']}")
 | 
			
		||||
 | 
			
		||||
                        # Parse the license plate result message
 | 
			
		||||
                        data = json.loads(message['data'])
 | 
			
		||||
                        logger.info(f"[LICENSE PLATE PARSED] Parsed JSON data: {data}")
 | 
			
		||||
                        await self._process_license_plate_result(data)
 | 
			
		||||
                    except json.JSONDecodeError as e:
 | 
			
		||||
                        logger.error(f"[LICENSE PLATE ERROR] Invalid JSON in license plate message: {e}")
 | 
			
		||||
                        logger.error(f"[LICENSE PLATE ERROR] Raw message was: {message['data']}")
 | 
			
		||||
                    except Exception as e:
 | 
			
		||||
                        logger.error(f"Error processing license plate message: {e}", exc_info=True)
 | 
			
		||||
 | 
			
		||||
        except asyncio.CancelledError:
 | 
			
		||||
            logger.info("License plate subscription task cancelled")
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(f"Error in license plate message listener: {e}", exc_info=True)
 | 
			
		||||
 | 
			
		||||
    async def _process_license_plate_result(self, data: Dict[str, Any]):
 | 
			
		||||
        """
 | 
			
		||||
        Process incoming license plate result from LPR service.
 | 
			
		||||
 | 
			
		||||
        Expected message format (from actual LPR service):
 | 
			
		||||
        {
 | 
			
		||||
            "session_id": "511",
 | 
			
		||||
            "license_character": "ข3184"
 | 
			
		||||
        }
 | 
			
		||||
        or
 | 
			
		||||
        {
 | 
			
		||||
            "session_id": "508",
 | 
			
		||||
            "display_id": "test3",
 | 
			
		||||
            "license_plate_text": "ABC-123",
 | 
			
		||||
            "confidence": 0.95,
 | 
			
		||||
            "timestamp": "2025-09-24T21:10:00Z"
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            data: License plate result data
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            session_id = data.get('session_id')
 | 
			
		||||
            if not session_id:
 | 
			
		||||
                logger.warning("License plate result missing session_id")
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            # Handle different message formats
 | 
			
		||||
            # Format 1: {"session_id": "511", "license_character": "ข3184"}
 | 
			
		||||
            # Format 2: {"session_id": "508", "license_plate_text": "ABC-123", "confidence": 0.95, ...}
 | 
			
		||||
            license_plate_text = data.get('license_plate_text') or data.get('license_character')
 | 
			
		||||
            confidence = data.get('confidence', 1.0)  # Default confidence for LPR service results
 | 
			
		||||
            display_id = data.get('display_id', '')
 | 
			
		||||
            timestamp = data.get('timestamp', '')
 | 
			
		||||
 | 
			
		||||
            logger.info(f"[LICENSE PLATE] Received result for session {session_id}: "
 | 
			
		||||
                       f"text='{license_plate_text}', confidence={confidence:.3f}")
 | 
			
		||||
 | 
			
		||||
            # Store in cache
 | 
			
		||||
            self.license_plate_cache[session_id] = {
 | 
			
		||||
                'license_plate_text': license_plate_text,
 | 
			
		||||
                'confidence': confidence,
 | 
			
		||||
                'display_id': display_id,
 | 
			
		||||
                'timestamp': timestamp,
 | 
			
		||||
                'received_at': asyncio.get_event_loop().time()
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            # Call callback if provided
 | 
			
		||||
            if self.callback:
 | 
			
		||||
                await self.callback(session_id, {
 | 
			
		||||
                    'license_plate_text': license_plate_text,
 | 
			
		||||
                    'confidence': confidence,
 | 
			
		||||
                    'display_id': display_id,
 | 
			
		||||
                    'timestamp': timestamp
 | 
			
		||||
                })
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(f"Error processing license plate result: {e}", exc_info=True)
 | 
			
		||||
 | 
			
		||||
    def get_license_plate_result(self, session_id: str) -> Optional[Dict[str, Any]]:
 | 
			
		||||
        """
 | 
			
		||||
        Get cached license plate result for a session.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            session_id: Session identifier
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            License plate result dictionary or None if not found
 | 
			
		||||
        """
 | 
			
		||||
        if session_id not in self.license_plate_cache:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        result = self.license_plate_cache[session_id]
 | 
			
		||||
 | 
			
		||||
        # Check TTL
 | 
			
		||||
        current_time = asyncio.get_event_loop().time()
 | 
			
		||||
        if current_time - result.get('received_at', 0) > self.cache_ttl:
 | 
			
		||||
            # Expired, remove from cache
 | 
			
		||||
            del self.license_plate_cache[session_id]
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            'license_plate_text': result.get('license_plate_text'),
 | 
			
		||||
            'confidence': result.get('confidence'),
 | 
			
		||||
            'display_id': result.get('display_id'),
 | 
			
		||||
            'timestamp': result.get('timestamp')
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def cleanup_expired_results(self):
 | 
			
		||||
        """Remove expired license plate results from cache."""
 | 
			
		||||
        try:
 | 
			
		||||
            current_time = asyncio.get_event_loop().time()
 | 
			
		||||
            expired_sessions = []
 | 
			
		||||
 | 
			
		||||
            for session_id, result in self.license_plate_cache.items():
 | 
			
		||||
                if current_time - result.get('received_at', 0) > self.cache_ttl:
 | 
			
		||||
                    expired_sessions.append(session_id)
 | 
			
		||||
 | 
			
		||||
            for session_id in expired_sessions:
 | 
			
		||||
                del self.license_plate_cache[session_id]
 | 
			
		||||
                logger.debug(f"Removed expired license plate result for session {session_id}")
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(f"Error cleaning up expired license plate results: {e}", exc_info=True)
 | 
			
		||||
 | 
			
		||||
    async def close(self):
 | 
			
		||||
        """Close Redis connection and cleanup resources."""
 | 
			
		||||
        try:
 | 
			
		||||
            # Cancel subscription task first
 | 
			
		||||
            if self.subscription_task and not self.subscription_task.done():
 | 
			
		||||
                self.subscription_task.cancel()
 | 
			
		||||
                try:
 | 
			
		||||
                    await self.subscription_task
 | 
			
		||||
                except asyncio.CancelledError:
 | 
			
		||||
                    logger.debug("License plate subscription task cancelled successfully")
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    logger.warning(f"Error waiting for subscription task cancellation: {e}")
 | 
			
		||||
 | 
			
		||||
            # Close pubsub connection properly
 | 
			
		||||
            if self.pubsub:
 | 
			
		||||
                try:
 | 
			
		||||
                    # First unsubscribe from channels
 | 
			
		||||
                    await self.pubsub.unsubscribe('license_results')
 | 
			
		||||
                    # Then close the pubsub connection
 | 
			
		||||
                    await self.pubsub.aclose()
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    logger.warning(f"Error closing pubsub connection: {e}")
 | 
			
		||||
                finally:
 | 
			
		||||
                    self.pubsub = None
 | 
			
		||||
 | 
			
		||||
            # Close Redis connection
 | 
			
		||||
            if self.redis_client:
 | 
			
		||||
                try:
 | 
			
		||||
                    await self.redis_client.aclose()
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    logger.warning(f"Error closing Redis connection: {e}")
 | 
			
		||||
                finally:
 | 
			
		||||
                    self.redis_client = None
 | 
			
		||||
 | 
			
		||||
            # Clear cache
 | 
			
		||||
            self.license_plate_cache.clear()
 | 
			
		||||
 | 
			
		||||
            logger.info("License plate manager closed successfully")
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(f"Error closing license plate manager: {e}", exc_info=True)
 | 
			
		||||
 | 
			
		||||
    def get_statistics(self) -> Dict[str, Any]:
 | 
			
		||||
        """Get license plate manager statistics."""
 | 
			
		||||
        return {
 | 
			
		||||
            'cached_results': len(self.license_plate_cache),
 | 
			
		||||
            'connected': self.redis_client is not None,
 | 
			
		||||
            'subscribed': self.pubsub is not None,
 | 
			
		||||
            'host': self.host,
 | 
			
		||||
            'port': self.port
 | 
			
		||||
        }
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue