"""FastAPI service for the AvisSense sentiment model.""" import logging import time from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field from src.inference import SentimentAnalyzer from src.utils import clean_text MAX_INPUT_CHARS = 5000 logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logger = logging.getLogger("avissense") analyzer = SentimentAnalyzer() @asynccontextmanager async def lifespan(app: FastAPI): """Load the model once at startup and release it at shutdown.""" logger.info("Chargement du modele : %s ...", analyzer.model_id) start_time = time.perf_counter() analyzer.load() logger.info("Modele charge en %.1f s - API prete.", time.perf_counter() - start_time) yield analyzer.unload() logger.info("Ressources liberees, arret de l'API.") app = FastAPI( title="AvisSense - Analyse de sentiment d'avis cinema en francais", description=( "DistilCamemBERT fine-tune sur le dataset Allocine. " "Classification binaire positif/negatif avec score de confiance." ), version="1.1.0", lifespan=lifespan, ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) class ReviewInput(BaseModel): """Request body accepted by POST /predict.""" text: str = Field( ..., description="L'avis en francais a analyser", examples=["Ce film est incroyable"], ) class PredictionOutput(BaseModel): """Prediction returned by POST /predict.""" label: str = Field(description='"positif" ou "negatif"') confidence: float = Field(description="Probabilite de la classe predite") probabilities: dict[str, float] = Field(description="Probabilite de chaque classe") processing_time_ms: float = Field(description="Duree de l'inference en millisecondes") @app.get("/info", tags=["info"]) def api_info(): return { "name": "AvisSense API", "version": "1.1.0", "description": "Analyse de sentiment d'avis cinema en francais", "endpoints": { "GET /": "front minimal", "GET /info": "informations de l'API", "GET /health": "etat de l'API et du modele", "GET /docs": "documentation Swagger", "POST /predict": 'body {"text": "..."} -> label + confiance', }, } @app.get("/", tags=["info"]) def api_root(): return { "name": "AvisSense API", "status": "online", "docs": "/docs", "health": "/health", "predict": "/predict", "frontend": "Deploy the React frontend separately on Vercel.", } @app.get("/health", tags=["monitoring"]) def health_check(): return { "status": "ok", "model_loaded": analyzer.is_loaded, "model_id": analyzer.model_id, "device": analyzer.device, } @app.post("/predict", response_model=PredictionOutput, tags=["prediction"]) def predict_sentiment(review: ReviewInput): text = clean_text(review.text) if not text: raise HTTPException(status_code=400, detail="Le texte ne peut pas etre vide.") if len(text) > MAX_INPUT_CHARS: raise HTTPException( status_code=400, detail=f"Texte trop long ({len(text)} caracteres, max {MAX_INPUT_CHARS}).", ) if not analyzer.is_loaded: raise HTTPException(status_code=503, detail="Modele en cours de chargement.") start_time = time.perf_counter() result = analyzer.predict(text) elapsed_ms = round((time.perf_counter() - start_time) * 1000, 1) logger.info( 'Prediction : "%s..." -> %s (%.0f %%) en %.0f ms', text[:40], result["label"], result["confidence"] * 100, elapsed_ms, ) return PredictionOutput(**result, processing_time_ms=elapsed_ms)