1+ import os
2+ import time
3+ import logging
4+ from typing import Literal
5+
6+ from fastapi import FastAPI , HTTPException , Request
7+ from fastapi .middleware .cors import CORSMiddleware
8+ from fastapi .responses import FileResponse , JSONResponse
9+ from fastapi .staticfiles import StaticFiles
10+ from pydantic import BaseModel , field_validator
11+ from transformers import pipeline
12+
13+ # ---------------- Logging ----------------
14+ logging .basicConfig (
15+ level = logging .INFO ,
16+ format = "%(asctime)s %(levelname)s %(name)s - %(message)s" ,
17+ )
18+ logger = logging .getLogger ("sentiment-api" )
19+
20+ # ---------------- Config -----------------
21+ MODEL_NAME = os .getenv ("HF_MODEL" , "distilbert-base-uncased-finetuned-sst-2-english" )
22+
23+ # ---------------- App --------------------
24+ app = FastAPI (
25+ title = "Sentiment Analysis API" ,
26+ version = "1.0.0" ,
27+ description = (
28+ "Binary sentiment (positive/negative) with confidence using a lightweight CPU model: "
29+ f"{ MODEL_NAME } "
30+ ),
31+ )
32+
33+ # CORS (keep permissive for demo; restrict in production)
34+ app .add_middleware (
35+ CORSMiddleware ,
36+ allow_origins = ["*" ],
37+ allow_credentials = True ,
38+ allow_methods = ["*" ],
39+ allow_headers = ["*" ],
40+ )
41+
42+ # Serve static files (page HTML de test)
43+ STATIC_DIR = os .path .join (os .path .dirname (__file__ ), ".." , "static" )
44+ app .mount ("/static" , StaticFiles (directory = STATIC_DIR ), name = "static" )
45+
46+ @app .get ("/" , include_in_schema = False )
47+ def root_page ():
48+ """Serve the minimal HTML test page at the root."""
49+ index_path = os .path .join (STATIC_DIR , "index.html" )
50+ if not os .path .exists (index_path ):
51+ return JSONResponse ({"status" : "ok" , "model" : MODEL_NAME })
52+ return FileResponse (index_path )
53+
54+
55+ # Load pipeline once at startup
56+ try :
57+ logger .info (f"Loading model pipeline: { MODEL_NAME } " )
58+ sentiment_pipe = pipeline ("sentiment-analysis" , model = MODEL_NAME )
59+ logger .info ("Model pipeline loaded successfully." )
60+ except Exception as e :
61+ logger .exception ("Failed to load model pipeline." )
62+ raise e
63+
64+
65+ class SentimentRequest (BaseModel ):
66+ text : str
67+
68+ @field_validator ("text" )
69+ @classmethod
70+ def not_empty (cls , v : str ) -> str :
71+ if not v or not v .strip ():
72+ raise ValueError ("text must be a non-empty string" )
73+ return v .strip ()
74+
75+
76+ class SentimentResponse (BaseModel ):
77+ label : Literal ["positive" , "negative" ]
78+ confidence : float
79+ model : str
80+
81+
82+ @app .get ("/health" , summary = "Health check" )
83+ def health ():
84+ return {"status" : "ok" , "model" : MODEL_NAME }
85+
86+
87+ @app .post ("/predict" , response_model = SentimentResponse , summary = "Analyse de sentiments (binaire)" )
88+ def predict (req : SentimentRequest , request : Request ):
89+ """
90+ Entrée : { "text": "your sentence" }
91+ Sortie : { "label": "positive|negative", "confidence": 0.0-1.0, "model": "<model_name>" }
92+ """
93+ start = time .time ()
94+ try :
95+ result = sentiment_pipe (req .text )[0 ] # e.g., {'label': 'POSITIVE', 'score': 0.999}
96+ label = "positive" if result ["label" ].upper ().startswith ("POS" ) else "negative"
97+ confidence = float (result ["score" ])
98+ duration_ms = (time .time () - start ) * 1000.0
99+
100+ logger .info (
101+ "prediction success | bytes=%d | ms=%.2f | label=%s | conf=%.4f" ,
102+ len (req .text .encode ("utf-8" )),
103+ duration_ms ,
104+ label ,
105+ confidence ,
106+ )
107+
108+ return SentimentResponse (label = label , confidence = confidence , model = MODEL_NAME )
109+ except Exception as e :
110+ logger .exception ("prediction error" )
111+ raise HTTPException (status_code = 500 , detail = str (e ))
0 commit comments