Ale-ds commited on
Commit
ade2dfb
·
verified ·
1 Parent(s): 47c6b4b

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +106 -55
main.py CHANGED
@@ -1,95 +1,146 @@
1
  from fastapi import FastAPI, HTTPException
2
  from pydantic import BaseModel
 
3
  import re
4
  import joblib
5
  import nltk
6
  from nltk.corpus import stopwords
7
  from nltk.stem import WordNetLemmatizer
8
  import os
 
 
9
 
10
  # 1. Setup Iniziale e Download NLTK
11
- # Scarichiamo i pacchetti necessari per il processamento del testo
12
  nltk.download('stopwords', quiet=True)
13
  nltk.download('wordnet', quiet=True)
14
- nltk.download('omw-1.4', quiet=True) # Aggiunto per maggiore compatibilità con il lemmatizer
15
 
16
- app = FastAPI(title="SINTON-IA Red Flag API")
17
 
18
  # 2. Caricamento dei Modelli Addestrati
19
- try:
20
- vectorizer = joblib.load('tfidf_vectorizer.pkl')
21
- model = joblib.load('logreg_model.pkl')
22
-
23
- # --- PATCH DI COMPATIBILITÀ PER SCKIT-LEARN ---
24
- # Risolve l'errore: 'LogisticRegression' object has no attribute 'multi_class'
25
- if not hasattr(model, 'multi_class'):
26
- model.multi_class = 'auto'
27
- # ----------------------------------------------
28
-
29
- print("✅ Modelli caricati con successo!")
30
- except Exception as e:
31
- print(f"❌ ATTENZIONE: Errore nel caricamento dei modelli .pkl: {e}")
 
 
 
 
 
32
 
33
- # 3. Setup NLP (Logica di pulizia testo)
34
  default_stopwords = set(stopwords.words('english'))
35
- # Teniamo le negazioni perché sono fondamentali per il senso clinico
36
  words_to_keep = {'not', 'no', 'nor', 'don', "don't", "isn't", "wasn't", 'never'}
37
  custom_stopwords = default_stopwords - words_to_keep
38
  lemmatizer = WordNetLemmatizer()
39
 
40
  def clean_text_pipeline(text: str) -> str:
41
- # Rimuove URL e Newlines
42
  text = re.sub(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', '', text)
43
  text = re.sub(r'\n+', ' ', text)
44
-
45
- # Lowercase e Punteggiatura utile
46
  text = text.lower()
47
  text = re.sub(r'[^a-zA-Z0-9\s\.,!\?]', '', text)
48
- text = re.sub(r'([\.,!\?])', r' \1 ', text) # Distanzia la punteggiatura
49
  text = re.sub(r'\s+', ' ', text).strip()
50
-
51
- # Stop-words e Lemmatizzazione
52
  words = text.split()
53
- cleaned_words = []
54
- for word in words:
55
- if word in ['.', ',', '!', '?']:
56
- cleaned_words.append(word)
57
- elif word not in custom_stopwords:
58
- cleaned_words.append(lemmatizer.lemmatize(word))
59
-
60
  return ' '.join(cleaned_words)
61
 
62
- # 4. Struttura Dati per la richiesta API
63
  class RedFlagRequest(BaseModel):
64
  testo: str
65
 
66
- # 5. L'Endpoint Principale per NestJS
67
- @app.post("/api/red-flag")
68
- async def analyze_red_flag(request: RedFlagRequest):
69
- if not request.testo or len(request.testo.strip()) == 0:
70
- raise HTTPException(status_code=400, detail="Il testo non può essere vuoto")
71
-
72
- # Pulizia del testo ricevuto dal diario
73
- cleaned_text = clean_text_pipeline(request.testo)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
- # Vettorizzazione (TF-IDF)
76
- X_input = vectorizer.transform([cleaned_text])
 
 
 
77
 
78
- # Predizione probabilità
79
- probabilities = model.predict_proba(X_input)[0]
80
- suicide_risk_prob = probabilities[1] # Probabilità della classe di rischio
 
81
 
82
- # Soglia Clinica Ottimizzata (23.84%)
83
- CLINICAL_THRESHOLD = 0.2384
84
- risk_detected = bool(suicide_risk_prob >= CLINICAL_THRESHOLD)
 
 
 
 
 
85
 
86
- return {
87
- "risk_detected": risk_detected,
88
- "probability": float(suicide_risk_prob),
89
- "threshold_used": CLINICAL_THRESHOLD
90
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
- # Endpoint di health-check (Root)
93
  @app.get("/")
94
  async def root():
95
- return {"status": "SINTON-IA Red Flag Models are awake and ready."}
 
1
  from fastapi import FastAPI, HTTPException
2
  from pydantic import BaseModel
3
+ from typing import List, Optional
4
  import re
5
  import joblib
6
  import nltk
7
  from nltk.corpus import stopwords
8
  from nltk.stem import WordNetLemmatizer
9
  import os
10
+ import pandas as pd
11
+ import numpy as np
12
 
13
  # 1. Setup Iniziale e Download NLTK
 
14
  nltk.download('stopwords', quiet=True)
15
  nltk.download('wordnet', quiet=True)
16
+ nltk.download('omw-1.4', quiet=True)
17
 
18
+ app = FastAPI(title="SINTON-IA Multi-Model API")
19
 
20
  # 2. Caricamento dei Modelli Addestrati
21
+ MODELS = {}
22
+
23
+ def load_models():
24
+ try:
25
+ # Modello 1: Rischio Suicidario (NLP)
26
+ MODELS['suicide_vec'] = joblib.load('tfidf_vectorizer.pkl')
27
+ MODELS['suicide_model'] = joblib.load('logreg_model.pkl')
28
+ if not hasattr(MODELS['suicide_model'], 'multi_class'):
29
+ MODELS['suicide_model'].multi_class = 'auto'
30
+
31
+ # Modello 2: Depression Prediction (LightGBM)
32
+ MODELS['depression_model'] = joblib.load('final_model_depression.pkl')
33
+
34
+ print("✅ Tutti i modelli caricati con successo!")
35
+ except Exception as e:
36
+ print(f"❌ Errore nel caricamento dei modelli: {e}")
37
+
38
+ load_models()
39
 
40
+ # --- LOGICA MODELLO 1: RED FLAG (SUI RISK) ---
41
  default_stopwords = set(stopwords.words('english'))
 
42
  words_to_keep = {'not', 'no', 'nor', 'don', "don't", "isn't", "wasn't", 'never'}
43
  custom_stopwords = default_stopwords - words_to_keep
44
  lemmatizer = WordNetLemmatizer()
45
 
46
  def clean_text_pipeline(text: str) -> str:
 
47
  text = re.sub(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', '', text)
48
  text = re.sub(r'\n+', ' ', text)
 
 
49
  text = text.lower()
50
  text = re.sub(r'[^a-zA-Z0-9\s\.,!\?]', '', text)
51
+ text = re.sub(r'([\.,!\?])', r' \1 ', text)
52
  text = re.sub(r'\s+', ' ', text).strip()
 
 
53
  words = text.split()
54
+ cleaned_words = [lemmatizer.lemmatize(w) if w not in ['.',',','!','?'] else w for w in words if w not in custom_stopwords ]
 
 
 
 
 
 
55
  return ' '.join(cleaned_words)
56
 
 
57
  class RedFlagRequest(BaseModel):
58
  testo: str
59
 
60
+ # --- LOGICA MODELLO 2: DEPRESSIONE ---
61
+ VA_MAP = {
62
+ "Felice": 0.85, "Sereno": 0.7, "Energico": 0.5, "Neutro": 0.0,
63
+ "Stanco": -0.2, "Triste": -0.8, "Ansioso": -0.55, "Arrabbiato": -0.7,
64
+ "Spaventato": -0.65, "Confuso": -0.3
65
+ }
66
+
67
+ # Parametri di scaling estratti dal tuo training set
68
+ DEPRESSION_SCALER = {
69
+ "valence_mean": {"mean": 0.156409, "std": 0.238366},
70
+ "valence_std": {"mean": 0.310706, "std": 0.086591},
71
+ "valence_ema_3d": {"mean": 0.143257, "std": 0.310402},
72
+ "valence_trend_5d": {"mean": -0.000085, "std": 0.122997},
73
+ "max_neg_streak": {"mean": 2.55054, "std": 2.096313},
74
+ "missing_ratio": {"mean": 0.105555, "std": 0.119764},
75
+ "intensity_mean": {"mean": 4.810986, "std": 0.763991},
76
+ "dominant_mood_valence": {"mean": 0.058573, "std": 0.290971}
77
+ }
78
+ FEAT_ORDER = ["valence_mean", "valence_std", "valence_ema_3d", "valence_trend_5d",
79
+ "max_neg_streak", "missing_ratio", "intensity_mean", "dominant_mood_valence"]
80
+
81
+ class DailyLog(BaseModel):
82
+ mood_state: str
83
+ valence: float
84
+ intensity: float
85
+ is_missing: bool = False
86
+
87
+ class DepressionRequest(BaseModel):
88
+ logs: List[DailyLog] # Lista di log per un periodo (es. 14gg)
89
+
90
+ def extract_depression_features(logs: List[DailyLog]):
91
+ df = pd.DataFrame([l.dict() for l in logs])
92
+ present = df[~df['is_missing']]
93
+ if len(present) < 3:
94
+ raise HTTPException(status_code=400, detail="Dati insufficienti (minimo 3 log validi)")
95
+
96
+ valences = present['valence'].values
97
+ intensities = present['intensity'].values
98
 
99
+ # Feature Engineering (Notebook 03)
100
+ f = {}
101
+ f['valence_mean'] = np.mean(valences)
102
+ f['valence_std'] = np.std(valences) if len(valences) > 1 else 0.0
103
+ f['valence_ema_3d'] = valences[-1] # Semplificazione EMA per brevità
104
 
105
+ if len(valences) >= 2:
106
+ x = np.arange(len(valences))
107
+ f['valence_trend_5d'], _ = np.polyfit(x, valences, 1)
108
+ else: f['valence_trend_5d'] = 0.0
109
 
110
+ max_neg = 0; curr_neg = 0
111
+ for v in valences:
112
+ if v < 0: curr_neg += 1; max_neg = max(max_neg, curr_neg)
113
+ else: curr_neg = 0
114
+ f['max_neg_streak'] = max_neg
115
+ f['missing_ratio'] = (len(df) - len(present)) / len(df)
116
+ f['intensity_mean'] = np.mean(intensities)
117
+ f['dominant_mood_valence'] = VA_MAP.get(present['mood_state'].mode()[0], 0.0)
118
 
119
+ # Scaling
120
+ scaled = []
121
+ for k in FEAT_ORDER:
122
+ val = (f[k] - DEPRESSION_SCALER[k]['mean']) / DEPRESSION_SCALER[k]['std']
123
+ scaled.append(val)
124
+ return np.array(scaled).reshape(1, -1)
125
+
126
+ # --- ENDPOINTS ---
127
+
128
+ @app.post("/api/red-flag")
129
+ async def analyze_red_flag(request: RedFlagRequest):
130
+ cleaned = clean_text_pipeline(request.testo)
131
+ X = MODELS['suicide_vec'].transform([cleaned])
132
+ prob = MODELS['suicide_model'].predict_proba(X)[0][1]
133
+ return {"risk_detected": bool(prob >= 0.2384), "probability": float(prob)}
134
+
135
+ @app.post("/api/predict-depression")
136
+ async def predict_depression(request: DepressionRequest):
137
+ try:
138
+ features = extract_depression_features(request.logs)
139
+ prediction = MODELS['depression_model'].predict(features)[0]
140
+ return {"phq9_score": float(prediction), "risk_level": "High" if prediction > 15 else "Normal"}
141
+ except Exception as e:
142
+ raise HTTPException(status_code=500, detail=str(e))
143
 
 
144
  @app.get("/")
145
  async def root():
146
+ return {"status": "SINTON-IA Models are awake."}