Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import os | |
| import sys | |
| import logging | |
| from pathlib import Path | |
| from typing import List | |
| import joblib | |
| import numpy as np | |
| from fastapi import FastAPI, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel, Field | |
| # Configure logging | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format='[%(asctime)s] %(name)s β %(levelname)s: %(message)s' | |
| ) | |
| logger = logging.getLogger(__name__) | |
| app = FastAPI( | |
| title="CyHub Model 4 β Domain Classification", | |
| description="Multi-class web data classifier (Normal/Adult/Betting/Malware)", | |
| version="1.0.0" | |
| ) | |
| # CORS configuration | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Configuration | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Expected feature count (must match training data) | |
| EXPECTED_FEATURES = 5 | |
| # Model file paths (support both relative and absolute) | |
| MODEL_DIR = Path(os.getenv("MODEL_DIR", ".")) | |
| MODEL_PATH = MODEL_DIR / "trained_lightgbm_model.pkl" | |
| ENCODER_PATH = MODEL_DIR / "label_encoder.pkl" | |
| logger.info(f"Looking for model at: {MODEL_PATH.absolute()}") | |
| logger.info(f"Looking for encoder at: {ENCODER_PATH.absolute()}") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Request/Response Models | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class InputData(BaseModel): | |
| """Input data for model prediction. | |
| Must be preprocessed (scaled, imputed, encoded) to match training data. | |
| """ | |
| features: List[float] = Field( | |
| ..., | |
| min_items=EXPECTED_FEATURES, | |
| max_items=EXPECTED_FEATURES, | |
| description=f"Exactly {EXPECTED_FEATURES} preprocessed float values" | |
| ) | |
| class PredictionResponse(BaseModel): | |
| """Model prediction response.""" | |
| predicted_label: str | |
| raw_prediction_encoded: int | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Model Loading | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| model = None | |
| label_encoder = None | |
| model_loaded = False | |
| def load_models(): | |
| """Load LightGBM model and label encoder.""" | |
| global model, label_encoder, model_loaded | |
| try: | |
| # Check if files exist | |
| if not MODEL_PATH.exists(): | |
| raise FileNotFoundError(f"Model file not found: {MODEL_PATH.absolute()}") | |
| if not ENCODER_PATH.exists(): | |
| raise FileNotFoundError(f"Encoder file not found: {ENCODER_PATH.absolute()}") | |
| # Load model | |
| logger.info(f"Loading model from {MODEL_PATH.absolute()}...") | |
| model = joblib.load(str(MODEL_PATH)) | |
| logger.info("β Model loaded successfully") | |
| # Load label encoder | |
| logger.info(f"Loading label encoder from {ENCODER_PATH.absolute()}...") | |
| label_encoder = joblib.load(str(ENCODER_PATH)) | |
| logger.info("β Label encoder loaded successfully") | |
| # Verify encoder has classes | |
| if not hasattr(label_encoder, 'classes_'): | |
| raise ValueError("Label encoder missing 'classes_' attribute") | |
| logger.info(f"Label classes: {label_encoder.classes_.tolist()}") | |
| model_loaded = True | |
| return True | |
| except FileNotFoundError as e: | |
| logger.error(f"File error: {e}") | |
| logger.warning(f"Make sure model files are in: {MODEL_DIR.absolute()}") | |
| return False | |
| except Exception as e: | |
| logger.error(f"Error loading models: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| return False | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Startup/Shutdown Events | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def startup_event(): | |
| """Initialize models on startup.""" | |
| global model_loaded | |
| logger.info("Starting up CyHub Model 4 API...") | |
| if not load_models(): | |
| logger.error("Failed to load models. API will return 503 until models are available.") | |
| model_loaded = False | |
| else: | |
| logger.info("Model 4 API ready!") | |
| async def shutdown_event(): | |
| """Cleanup on shutdown.""" | |
| logger.info("Shutting down CyHub Model 4 API...") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # API Endpoints | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def root(): | |
| """Root endpoint β health check.""" | |
| return { | |
| "service": "CyHub Model 4 β Domain Classification", | |
| "status": "healthy", | |
| "model_loaded": model_loaded, | |
| "version": "1.0.0" | |
| } | |
| async def health_check(): | |
| """Health check endpoint.""" | |
| if not model_loaded: | |
| raise HTTPException( | |
| status_code=503, | |
| detail="Model not loaded. Check server logs." | |
| ) | |
| return { | |
| "status": "healthy", | |
| "model_status": "ready", | |
| "expected_features": EXPECTED_FEATURES | |
| } | |
| async def predict(data: InputData) -> PredictionResponse: | |
| """Predict domain classification from preprocessed features. | |
| Args: | |
| data: InputData with exactly 5 preprocessed features | |
| Returns: | |
| PredictionResponse with predicted_label and raw_prediction_encoded | |
| Raises: | |
| HTTPException: If model not loaded or feature count incorrect | |
| """ | |
| # Check if model is loaded | |
| if not model_loaded or model is None or label_encoder is None: | |
| logger.error("Model not loaded, cannot make prediction") | |
| raise HTTPException( | |
| status_code=503, | |
| detail="Model not loaded. Check server logs." | |
| ) | |
| try: | |
| # Validate feature count | |
| if len(data.features) != EXPECTED_FEATURES: | |
| raise ValueError( | |
| f"Expected {EXPECTED_FEATURES} features, got {len(data.features)}" | |
| ) | |
| # Validate all features are float/convertible | |
| features_array = np.array(data.features, dtype=float) | |
| # Check for NaN or Inf | |
| if not np.all(np.isfinite(features_array)): | |
| raise ValueError("Features contain NaN or Inf values") | |
| # Reshape for prediction (1 sample, 5 features) | |
| input_array = features_array.reshape(1, -1) | |
| logger.debug(f"Input features: {data.features}") | |
| # Make prediction | |
| prediction_encoded = model.predict(input_array)[0] | |
| logger.debug(f"Raw prediction: {prediction_encoded}") | |
| # Ensure prediction is valid | |
| if prediction_encoded not in label_encoder.classes_: | |
| logger.warning(f"Unexpected prediction value: {prediction_encoded}") | |
| # Decode prediction to label | |
| try: | |
| prediction_label = label_encoder.inverse_transform([prediction_encoded])[0] | |
| except Exception as e: | |
| logger.error(f"Error decoding prediction: {e}") | |
| raise ValueError(f"Could not decode prediction {prediction_encoded}") | |
| logger.info(f"Prediction: {prediction_label}") | |
| return PredictionResponse( | |
| predicted_label=prediction_label, | |
| raw_prediction_encoded=int(prediction_encoded) | |
| ) | |
| except ValueError as e: | |
| logger.error(f"Validation error: {e}") | |
| raise HTTPException( | |
| status_code=400, | |
| detail=str(e) | |
| ) | |
| except Exception as e: | |
| logger.error(f"Prediction error: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| raise HTTPException( | |
| status_code=500, | |
| detail=f"Prediction failed: {str(e)}" | |
| ) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Development Entry Point | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == "__main__": | |
| import uvicorn | |
| # For development/testing | |
| logger.info("Starting Model 4 API in development mode...") | |
| uvicorn.run( | |
| app, | |
| host="0.0.0.0", | |
| port=7860, # Default HuggingFace Spaces port | |
| log_level="info" | |
| ) | |