""" Detector Worker - Main FastAPI Application Refactored modular architecture for computer vision pipeline processing. """ import json import logging import os import time from contextlib import asynccontextmanager from fastapi import FastAPI, WebSocket, HTTPException, Request from fastapi.responses import Response # Import new modular communication system from core.communication.websocket import websocket_endpoint from core.communication.state import worker_state # Configure logging logging.basicConfig( level=logging.DEBUG, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", handlers=[ logging.FileHandler("detector_worker.log"), logging.StreamHandler() ] ) logger = logging.getLogger("detector_worker") logger.setLevel(logging.DEBUG) # Store cached frames for REST API access (temporary storage) latest_frames = {} # Lifespan event handler (modern FastAPI approach) @asynccontextmanager async def lifespan(app: FastAPI): """Application lifespan management.""" # Startup logger.info("Detector Worker started successfully") logger.info("WebSocket endpoint available at: ws://0.0.0.0:8001/") logger.info("HTTP camera endpoint available at: http://0.0.0.0:8001/camera/{camera_id}/image") logger.info("Health check available at: http://0.0.0.0:8001/health") logger.info("Ready and waiting for backend WebSocket connections") yield # Shutdown logger.info("Detector Worker shutting down...") # Clear all state worker_state.set_subscriptions([]) worker_state.session_ids.clear() worker_state.progression_stages.clear() latest_frames.clear() logger.info("Detector Worker shutdown complete") # Create FastAPI application with detailed WebSocket logging app = FastAPI(title="Detector Worker", version="2.0.0", lifespan=lifespan) # Add middleware to log all requests @app.middleware("http") async def log_requests(request, call_next): start_time = time.time() response = await call_next(request) process_time = time.time() - start_time logger.debug(f"HTTP {request.method} {request.url} - {response.status_code} ({process_time:.3f}s)") return response # Load configuration config_path = "config.json" if os.path.exists(config_path): with open(config_path, "r") as f: config = json.load(f) logger.info(f"Loaded configuration from {config_path}") else: # Default configuration config = { "poll_interval_ms": 100, "reconnect_interval_sec": 5, "target_fps": 10, "max_streams": 5, "max_retries": 3 } logger.warning(f"Configuration file {config_path} not found, using defaults") # Ensure models directory exists os.makedirs("models", exist_ok=True) logger.info("Ensured models directory exists") # Store cached frames for REST API access (temporary storage) latest_frames = {} logger.info("Starting detector worker application (refactored)") logger.info(f"Configuration: Target FPS: {config.get('target_fps', 10)}, " f"Max streams: {config.get('max_streams', 5)}, " f"Max retries: {config.get('max_retries', 3)}") @app.websocket("/") async def websocket_handler(websocket: WebSocket): """ Main WebSocket endpoint for backend communication. Handles all protocol messages according to worker.md specification. """ client_info = f"{websocket.client.host}:{websocket.client.port}" if websocket.client else "unknown" logger.info(f"[RX ← Backend] New WebSocket connection request from {client_info}") try: await websocket_endpoint(websocket) except Exception as e: logger.error(f"WebSocket handler error for {client_info}: {e}", exc_info=True) @app.get("/camera/{camera_id}/image") async def get_camera_image(camera_id: str): """ HTTP endpoint to retrieve the latest frame from a camera as JPEG image. This endpoint is preserved for backward compatibility with existing systems. Args: camera_id: The subscription identifier (e.g., "display-001;cam-001") Returns: JPEG image as binary response Raises: HTTPException: 404 if camera not found or no frame available HTTPException: 500 if encoding fails """ try: from urllib.parse import unquote # URL decode the camera_id to handle encoded characters original_camera_id = camera_id camera_id = unquote(camera_id) logger.debug(f"REST API request: original='{original_camera_id}', decoded='{camera_id}'") # Check if camera is in active subscriptions subscription = worker_state.get_subscription(camera_id) if not subscription: logger.warning(f"Camera ID '{camera_id}' not found in active subscriptions") available_cameras = list(worker_state.subscriptions.keys()) logger.debug(f"Available cameras: {available_cameras}") raise HTTPException( status_code=404, detail=f"Camera {camera_id} not found or not active" ) # Check if we have a cached frame for this camera if camera_id not in latest_frames: logger.warning(f"No cached frame available for camera '{camera_id}'") raise HTTPException( status_code=404, detail=f"No frame available for camera {camera_id}" ) frame = latest_frames[camera_id] logger.debug(f"Retrieved cached frame for camera '{camera_id}', shape: {frame.shape}") # TODO: This import will be replaced in Phase 3 (Streaming System) # For now, we need to handle the case where OpenCV is not available try: import cv2 # Encode frame as JPEG success, buffer_img = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 85]) if not success: raise HTTPException(status_code=500, detail="Failed to encode image as JPEG") # Return image as binary response return Response(content=buffer_img.tobytes(), media_type="image/jpeg") except ImportError: logger.error("OpenCV not available for image encoding") raise HTTPException(status_code=500, detail="Image processing not available") except HTTPException: raise except Exception as e: logger.error(f"Error retrieving image for camera {camera_id}: {str(e)}", exc_info=True) raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") @app.get("/health") async def health_check(): """Health check endpoint for monitoring.""" return { "status": "healthy", "version": "2.0.0", "active_subscriptions": len(worker_state.subscriptions), "active_sessions": len(worker_state.session_ids) } if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8001)