cassandrasestier commited on
Commit
19a82d9
·
verified ·
1 Parent(s): d1fb552

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +65 -250
app.py CHANGED
@@ -19,7 +19,7 @@ from sklearn.multiclass import OneVsRestClassifier
19
  from sklearn.pipeline import Pipeline
20
  from sklearn.metrics import f1_score
21
 
22
- # ---------------- Storage paths (robust local vs. HF Spaces) ----------------
23
  def _pick_data_dir():
24
  if os.path.isdir("/data") and os.access("/data", os.W_OK):
25
  return "/data"
@@ -28,8 +28,8 @@ def _pick_data_dir():
28
  DATA_DIR = os.getenv("MM_DATA_DIR", _pick_data_dir())
29
  os.makedirs(DATA_DIR, exist_ok=True)
30
  DB_PATH = os.path.join(DATA_DIR, "moodmirror.db")
31
- MODEL_PATH = os.path.join(DATA_DIR, "goemo_sklearn.joblib") # pipeline + mlb
32
- MODEL_VERSION = "v1-tfidf-lr-ovr" # bump if you change training
33
 
34
  print(f"[MM] Using data dir: {DATA_DIR}")
35
  print(f"[MM] SQLite path: {DB_PATH}")
@@ -48,7 +48,7 @@ CRISIS_NUMBERS = {
48
  "Other / Not listed": "Call your local emergency number (**112/911**) or search “suicide crisis hotline” + your country.",
49
  }
50
 
51
- # --- Deep, varied emotional advice & comfort phrases ---
52
  SUGGESTIONS = {
53
  "sadness": [
54
  "Be gentle with yourself. Cry if you need to — that’s healing, not weakness.",
@@ -162,66 +162,53 @@ SUGGESTIONS = {
162
  ],
163
  }
164
 
165
- # --- Inspirational / comforting quotes & affirmations ---
166
  QUOTES = {
167
  "sadness": [
168
  "“Even the darkest night will end and the sun will rise.” – Victor Hugo",
169
- "“You don’t have to feel better to start healing.”",
170
- "“It’s okay to be lost for a while.”",
171
- "“Tears are words the heart can’t express.” – Paulo Coelho",
172
  "“You have survived every hard day so far.”",
 
173
  ],
174
  "fear": [
175
  "“Feel the fear and do it anyway.” – Susan Jeffers",
176
- "“Courage is not the absence of fear, but acting in spite of it.”",
177
- "“You’ve faced hard things before — you can again.”",
178
  "“This moment will not last forever.”",
 
179
  ],
180
  "joy": [
181
  "“Happiness is not out there, it’s in you.”",
182
  "“Let joy be your rebellion.”",
183
  "“Enjoy the little things — one day you’ll realize they were the big things.”",
184
- "“Joy shared is joy doubled.”",
185
  ],
186
  "anger": [
187
- "“Speak when you are angry and you’ll make the best speech you’ll ever regret.” – Ambrose Bierce",
188
  "“Peace begins with a pause.”",
189
- "“Anger is energy — learn to guide it, not suppress it.”",
190
  ],
191
  "boredom": [
192
  "“Boredom is the beginning of imagination.” – Jules Renard",
193
  "“Curiosity is the cure for boredom.” – Dorothy Parker",
194
- "“The small things done repeatedly change everything.”",
195
  ],
196
  "grief": [
197
  "“Grief is love that has nowhere to go.”",
198
- "“What we once enjoyed we can never lose; all that we love deeply becomes part of us.” – Helen Keller",
199
  "“Love doesn’t end, it changes form.”",
200
  ],
201
  "love": [
202
  "“Where there is love, there is life.” – Mahatma Gandhi",
203
  "“You are loved just for being who you are.” – Ram Dass",
204
- "“Love quietly transforms everything it touches.”",
205
  ],
206
  "nervousness": [
207
- "“You don’t have to control your thoughts; just stop letting them control you.” – Dan Millman",
208
  "“Breathe. You are doing enough.”",
209
  "“This worry does not define you.”",
210
  ],
211
  "curiosity": [
212
  "“Stay curious — it’s the mind’s way of loving life.”",
213
- "“Wonder is wisdom’s beginning.” – Socrates",
214
  "“Every question plants a seed.”",
215
  ],
216
  "gratitude": [
217
  "“Gratitude turns what we have into enough.”",
218
- "“The more grateful I am, the more beauty I see.” – Mary Davis",
219
  "“Thankfulness unlocks joy.”",
220
  ],
221
  "neutral": [
222
  "“Be present — even a calm moment can be a quiet victory.”",
223
  "“Peace is not the absence of chaos, but the presence of inner calm.”",
224
- "“Slow is smooth, smooth is peaceful.”",
225
  ],
226
  }
227
 
@@ -233,216 +220,118 @@ COLOR_MAP = {
233
  "neutral": "#F5F5F5",
234
  }
235
 
236
- # Map GoEmotions label -> your UI buckets
237
  GOEMO_TO_APP = {
238
- "admiration": "gratitude",
239
- "amusement": "joy",
240
- "anger": "anger",
241
- "annoyance": "anger",
242
- "approval": "gratitude",
243
- "caring": "love",
244
- "confusion": "nervousness",
245
- "curiosity": "curiosity",
246
- "desire": "joy",
247
- "disappointment": "sadness",
248
- "disapproval": "anger",
249
- "disgust": "anger",
250
- "embarrassment": "nervousness",
251
- "excitement": "joy",
252
- "fear": "fear",
253
- "gratitude": "gratitude",
254
- "grief": "grief",
255
- "joy": "joy",
256
- "love": "love",
257
- "nervousness": "nervousness",
258
- "optimism": "joy",
259
- "pride": "joy",
260
- "realization": "neutral",
261
- "relief": "gratitude",
262
- "remorse": "grief",
263
- "sadness": "sadness",
264
- "surprise": "neutral",
265
- "neutral": "neutral",
266
  }
267
 
268
- THRESHOLD = 0.30 # probability threshold for selecting labels
269
 
270
  # ---------------- SQLite helpers ----------------
271
  def get_conn():
272
  return sqlite3.connect(DB_PATH, check_same_thread=False, timeout=10)
273
 
274
  def init_db():
275
- conn = None
276
- try:
277
- conn = get_conn()
278
- c = conn.cursor()
279
- c.execute("""
280
- CREATE TABLE IF NOT EXISTS sessions(
281
- id INTEGER PRIMARY KEY AUTOINCREMENT,
282
- ts TEXT,
283
- country TEXT,
284
- user_text TEXT,
285
- main_emotion TEXT
286
- )
287
- """)
288
- conn.commit()
289
- finally:
290
- try:
291
- if conn: conn.close()
292
- except Exception:
293
- pass
294
 
295
  def log_session(country, msg, emotion):
296
- conn = None
297
- try:
298
- conn = get_conn()
299
- c = conn.cursor()
300
- c.execute(
301
- "INSERT INTO sessions(ts, country, user_text, main_emotion) VALUES(?,?,?,?)",
302
- (datetime.utcnow().isoformat(timespec="seconds"), country, msg[:500], emotion),
303
- )
304
- conn.commit()
305
- finally:
306
- try:
307
- if conn: conn.close()
308
- except Exception:
309
- pass
310
-
311
- # ---------------- Train / Load model from DATASET ONLY ----------------
312
  def load_goemotions_dataset():
313
- # "simplified" gives 'text' and 'labels' as list[int] indices
314
  ds = load_dataset("google-research-datasets/go_emotions", "simplified")
315
  label_names = ds["train"].features["labels"].feature.names
316
  return ds, label_names
317
 
318
- def _prepare_xy(split):
319
- # Each example has text and labels (list of ints)
320
- X = split["text"]
321
- y = split["labels"] # list[list[int]]
322
- return X, y
323
-
324
  def train_or_load_model():
325
- # Try cache first
326
  if os.path.isfile(MODEL_PATH):
327
  print("[MM] Loading cached classifier...")
328
  bundle = joblib.load(MODEL_PATH)
329
  if bundle.get("version") == MODEL_VERSION:
330
  return bundle["pipeline"], bundle["mlb"], bundle["label_names"]
331
- else:
332
- print("[MM] Cached model version mismatch; retraining...")
333
 
334
  print("[MM] Loading GoEmotions dataset...")
335
  ds, label_names = load_goemotions_dataset()
336
 
337
- print("[MM] Preparing data...")
338
- X_train, y_train_idx = _prepare_xy(ds["train"])
339
- X_val, y_val_idx = _prepare_xy(ds["validation"])
340
 
341
- # MultiLabelBinarizer to convert list[int] -> multi-hot
342
  mlb = MultiLabelBinarizer(classes=list(range(len(label_names))))
343
- Y_train = mlb.fit_transform(y_train_idx)
344
- Y_val = mlb.transform(y_val_idx)
345
-
346
- # Build pipeline
347
- clf = Pipeline(steps=[
348
- ("tfidf", TfidfVectorizer(
349
- lowercase=True,
350
- ngram_range=(1,2),
351
- min_df=2,
352
- max_df=0.9,
353
- strip_accents="unicode",
354
- )),
355
- ("ovr", OneVsRestClassifier(
356
- LogisticRegression(
357
- solver="saga",
358
- max_iter=1000,
359
- n_jobs=-1,
360
- class_weight="balanced",
361
- ),
362
- n_jobs=-1
363
- ))
364
  ])
365
 
366
- print("[MM] Training classifier (this happens once; cached afterward)...")
367
  clf.fit(X_train, Y_train)
368
 
369
- # Quick validation metric (macro F1 over labels present in val)
370
- Y_val_pred = clf.predict(X_val)
371
- macro_f1 = f1_score(Y_val, Y_val_pred, average="macro", zero_division=0)
372
- print(f"[MM] Validation macro F1: {macro_f1:.3f}")
373
-
374
- # Cache model
375
- joblib.dump({
376
- "version": MODEL_VERSION,
377
- "pipeline": clf,
378
- "mlb": mlb,
379
- "label_names": label_names
380
- }, MODEL_PATH)
381
- print(f"[MM] Saved classifier to {MODEL_PATH}")
382
 
 
383
  return clf, mlb, label_names
384
 
385
- # Train/load at startup
386
  try:
387
  CLASSIFIER, MLB, LABEL_NAMES = train_or_load_model()
388
  except Exception as e:
389
  print(f"[WARN] Failed to train/load classifier: {e}")
390
  CLASSIFIER, MLB, LABEL_NAMES = None, None, None
391
 
392
- # ---------------- Inference using ONLY the trained classifier ----------------
393
  def classify_text(text: str):
394
- """
395
- Returns list of (label_name, prob) for labels above THRESHOLD, sorted desc.
396
- """
397
- if not CLASSIFIER or not MLB or not LABEL_NAMES:
398
- return []
399
-
400
- # predict_proba returns array shape (1, n_labels)
401
  try:
402
  proba = CLASSIFIER.predict_proba([text])[0]
403
  except AttributeError:
404
- # If estimator doesn't support predict_proba (shouldn't happen with LR),
405
- # fall back to decision_function -> sigmoid
406
  from scipy.special import expit
407
- scores = CLASSIFIER.decision_function([text])[0]
408
- proba = expit(scores)
409
-
410
  idxs = [i for i, p in enumerate(proba) if p >= THRESHOLD]
411
- # Sort by probability desc
412
  idxs.sort(key=lambda i: proba[i], reverse=True)
413
  return [(LABEL_NAMES[i], float(proba[i])) for i in idxs]
414
 
415
  def detect_emotions(text: str):
416
  chosen = classify_text(text)
417
- if not chosen:
418
- return "neutral"
419
- # Map to app buckets and take the strongest
420
  bucket = {}
421
  for label, p in chosen:
422
  app = GOEMO_TO_APP.get(label.lower(), "neutral")
423
  bucket[app] = max(bucket.get(app, 0.0), p)
424
- main = max(bucket, key=bucket.get) if bucket else "neutral"
425
- return main
426
 
427
- # ---------------- Legacy-style reply composer (advice/quote/both) -----------
428
  def compose_support_legacy(main_emotion: str, is_first_msg: bool) -> str:
429
- tip = random.choice(SUGGESTIONS.get(
430
- main_emotion,
431
- ["Take a slow breath. One small act of kindness can shift your day."]
432
- ))
433
- quote = random.choice(QUOTES.get(
434
- main_emotion,
435
- ["“No matter what you feel right now, this moment will pass.”"]
436
- ))
437
-
438
- # 0 = advice only, 1 = quote only, 2 = both
439
- mode = random.choice([0, 1, 2])
440
- if mode == 0:
441
- reply = tip
442
- elif mode == 1:
443
- reply = f"✨ {quote}"
444
- else:
445
- reply = f"{tip}\n\n💬 {quote}"
446
 
447
  if is_first_msg:
448
  reply += "\n\n*Can you tell me a bit more about what’s behind that feeling?*"
@@ -452,88 +341,14 @@ def compose_support_legacy(main_emotion: str, is_first_msg: bool) -> str:
452
  # ---------------- Chat logic ----------------
453
  def crisis_block(country):
454
  msg = CRISIS_NUMBERS.get(country, CRISIS_NUMBERS["Other / Not listed"])
455
- return (
456
- "💛 I'm really sorry you're feeling like this. You matter.\n\n"
457
- f"**If you might be in danger or thinking about harming yourself:**\n{msg}\n\n"
458
- "Please reach out to someone now. You are not alone."
459
- )
460
 
461
  def chat_step(message, history, country, save_session):
462
  if CRISIS_RE.search(message):
463
  return crisis_block(country), "#FFD6E7"
464
-
465
  if CLOSING_RE.search(message):
466
  return ("Thank you 💛 Take care of yourself. Small steps matter. 🌿", "#FFFFFF")
467
 
468
- recent = " ".join(message.split()[-100:])
469
- main = detect_emotions(recent)
470
- color = COLOR_MAP.get(main, "#FFFFFF")
471
-
472
- if save_session:
473
- log_session(country, message, main)
474
-
475
- reply = compose_support_legacy(main, is_first_msg=not bool(history))
476
- return reply, color
477
-
478
- # ---------------- Gradio UI ----------------
479
- init_db()
480
-
481
- custom_css = """
482
- :root, body, .gradio-container { transition: background-color 0.8s ease !important; }
483
- .typing { font-style: italic; opacity: 0.8; animation: blink 1s infinite; }
484
- @keyframes blink { 50% {opacity: 0.4;} }
485
- """
486
-
487
- with gr.Blocks(css=custom_css, title="🪞 MoodMirror+ (Dataset-only Edition)") as demo:
488
- style_injector = gr.HTML("")
489
- gr.Markdown(
490
- "### 🪞 MoodMirror+ — Emotional Support & Inspiration 🌸\n"
491
- "Powered only by the **GoEmotions dataset** (trained locally on startup).\n\n"
492
- "_Not medical advice. If you feel unsafe, please reach out for help immediately._"
493
- )
494
-
495
- with gr.Row():
496
- country = gr.Dropdown(choices=list(CRISIS_NUMBERS.keys()), value="Other / Not listed", label="Country")
497
- save_ok = gr.Checkbox(value=False, label="Save anonymized session (no personal data)")
498
-
499
- chat = gr.Chatbot(height=360)
500
- msg = gr.Textbox(placeholder="Type how you feel...", label="Your message")
501
- send = gr.Button("Send")
502
- typing = gr.Markdown("", elem_classes="typing")
503
-
504
- # Optional: dataset preview (for transparency)
505
- with gr.Accordion("🔎 Preview GoEmotions samples", open=False):
506
- with gr.Row():
507
- n_examples = gr.Slider(1, 10, value=5, step=1, label="Number of examples")
508
- split = gr.Dropdown(["train", "validation", "test"], value="train", label="Split")
509
- refresh = gr.Button("Show samples")
510
- table = gr.Dataframe(headers=["text", "labels"], row_count=5, wrap=True)
511
-
512
- def refresh_samples(n, split_name):
513
- try:
514
- ds = load_dataset("google-research-datasets/go_emotions", "simplified")
515
- names = ds["train"].features["labels"].feature.names
516
- rows = ds[split_name].shuffle(seed=42).select(range(min(int(n), len(ds[split_name]))))
517
- return [[t, ", ".join([names[i] for i in labs])] for t, labs in zip(rows["text"], rows["labels"])]
518
- except Exception as e:
519
- return [[f"Dataset load error: {e}", ""]]
520
-
521
- refresh.click(refresh_samples, inputs=[n_examples, split], outputs=[table])
522
-
523
- def respond(user_msg, chat_hist, country_choice, save_flag):
524
- if not user_msg or not user_msg.strip():
525
- yield chat_hist + [[user_msg, "Please share a short sentence about how you feel 🙂"]], "", "", ""
526
- return
527
- yield chat_hist, "💭 MoodMirror is thinking...", "", ""
528
- reply, color = chat_step(user_msg, chat_hist, country_choice, bool(save_flag))
529
- style_tag = f"<style>:root,body,.gradio-container{{background:{color}!important;}}</style>"
530
- yield chat_hist + [[user_msg, reply]], "", style_tag, ""
531
-
532
- send.click(respond, inputs=[msg, chat, country, save_ok],
533
- outputs=[chat, typing, style_injector, msg], queue=True)
534
- msg.submit(respond, inputs=[msg, chat, country, save_ok],
535
- outputs=[chat, typing, style_injector, msg], queue=True)
536
-
537
- if __name__ == "__main__":
538
- demo.queue()
539
- demo.launch()
 
19
  from sklearn.pipeline import Pipeline
20
  from sklearn.metrics import f1_score
21
 
22
+ # ---------------- Storage paths ----------------
23
  def _pick_data_dir():
24
  if os.path.isdir("/data") and os.access("/data", os.W_OK):
25
  return "/data"
 
28
  DATA_DIR = os.getenv("MM_DATA_DIR", _pick_data_dir())
29
  os.makedirs(DATA_DIR, exist_ok=True)
30
  DB_PATH = os.path.join(DATA_DIR, "moodmirror.db")
31
+ MODEL_PATH = os.path.join(DATA_DIR, "goemo_sklearn.joblib")
32
+ MODEL_VERSION = "v1-tfidf-lr-ovr"
33
 
34
  print(f"[MM] Using data dir: {DATA_DIR}")
35
  print(f"[MM] SQLite path: {DB_PATH}")
 
48
  "Other / Not listed": "Call your local emergency number (**112/911**) or search “suicide crisis hotline” + your country.",
49
  }
50
 
51
+ # ---------------- Advice & Quotes ----------------
52
  SUGGESTIONS = {
53
  "sadness": [
54
  "Be gentle with yourself. Cry if you need to — that’s healing, not weakness.",
 
162
  ],
163
  }
164
 
 
165
  QUOTES = {
166
  "sadness": [
167
  "“Even the darkest night will end and the sun will rise.” – Victor Hugo",
 
 
 
168
  "“You have survived every hard day so far.”",
169
+ "“You don’t have to feel better to start healing.”",
170
  ],
171
  "fear": [
172
  "“Feel the fear and do it anyway.” – Susan Jeffers",
 
 
173
  "“This moment will not last forever.”",
174
+ "“You’ve faced hard things before — you can again.”",
175
  ],
176
  "joy": [
177
  "“Happiness is not out there, it’s in you.”",
178
  "“Let joy be your rebellion.”",
179
  "“Enjoy the little things — one day you’ll realize they were the big things.”",
 
180
  ],
181
  "anger": [
 
182
  "“Peace begins with a pause.”",
183
+ "“Anger is energy — guide it, don’t suppress it.”",
184
  ],
185
  "boredom": [
186
  "“Boredom is the beginning of imagination.” – Jules Renard",
187
  "“Curiosity is the cure for boredom.” – Dorothy Parker",
 
188
  ],
189
  "grief": [
190
  "“Grief is love that has nowhere to go.”",
 
191
  "“Love doesn’t end, it changes form.”",
192
  ],
193
  "love": [
194
  "“Where there is love, there is life.” – Mahatma Gandhi",
195
  "“You are loved just for being who you are.” – Ram Dass",
 
196
  ],
197
  "nervousness": [
 
198
  "“Breathe. You are doing enough.”",
199
  "“This worry does not define you.”",
200
  ],
201
  "curiosity": [
202
  "“Stay curious — it’s the mind’s way of loving life.”",
 
203
  "“Every question plants a seed.”",
204
  ],
205
  "gratitude": [
206
  "“Gratitude turns what we have into enough.”",
 
207
  "“Thankfulness unlocks joy.”",
208
  ],
209
  "neutral": [
210
  "“Be present — even a calm moment can be a quiet victory.”",
211
  "“Peace is not the absence of chaos, but the presence of inner calm.”",
 
212
  ],
213
  }
214
 
 
220
  "neutral": "#F5F5F5",
221
  }
222
 
 
223
  GOEMO_TO_APP = {
224
+ "admiration": "gratitude", "amusement": "joy", "anger": "anger", "annoyance": "anger",
225
+ "approval": "gratitude", "caring": "love", "confusion": "nervousness",
226
+ "curiosity": "curiosity", "desire": "joy", "disappointment": "sadness",
227
+ "disapproval": "anger", "disgust": "anger", "embarrassment": "nervousness",
228
+ "excitement": "joy", "fear": "fear", "gratitude": "gratitude", "grief": "grief",
229
+ "joy": "joy", "love": "love", "nervousness": "nervousness", "optimism": "joy",
230
+ "pride": "joy", "realization": "neutral", "relief": "gratitude", "remorse": "grief",
231
+ "sadness": "sadness", "surprise": "neutral", "neutral": "neutral",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  }
233
 
234
+ THRESHOLD = 0.30
235
 
236
  # ---------------- SQLite helpers ----------------
237
  def get_conn():
238
  return sqlite3.connect(DB_PATH, check_same_thread=False, timeout=10)
239
 
240
  def init_db():
241
+ conn = get_conn()
242
+ c = conn.cursor()
243
+ c.execute("""
244
+ CREATE TABLE IF NOT EXISTS sessions(
245
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
246
+ ts TEXT,
247
+ country TEXT,
248
+ user_text TEXT,
249
+ main_emotion TEXT
250
+ )
251
+ """)
252
+ conn.commit()
253
+ conn.close()
 
 
 
 
 
 
254
 
255
  def log_session(country, msg, emotion):
256
+ conn = get_conn()
257
+ c = conn.cursor()
258
+ c.execute("INSERT INTO sessions(ts, country, user_text, main_emotion) VALUES(?,?,?,?)",
259
+ (datetime.utcnow().isoformat(timespec="seconds"), country, msg[:500], emotion))
260
+ conn.commit()
261
+ conn.close()
262
+
263
+ # ---------------- Train / Load model ----------------
 
 
 
 
 
 
 
 
264
  def load_goemotions_dataset():
 
265
  ds = load_dataset("google-research-datasets/go_emotions", "simplified")
266
  label_names = ds["train"].features["labels"].feature.names
267
  return ds, label_names
268
 
 
 
 
 
 
 
269
  def train_or_load_model():
 
270
  if os.path.isfile(MODEL_PATH):
271
  print("[MM] Loading cached classifier...")
272
  bundle = joblib.load(MODEL_PATH)
273
  if bundle.get("version") == MODEL_VERSION:
274
  return bundle["pipeline"], bundle["mlb"], bundle["label_names"]
 
 
275
 
276
  print("[MM] Loading GoEmotions dataset...")
277
  ds, label_names = load_goemotions_dataset()
278
 
279
+ X_train = ds["train"]["text"]; y_train = ds["train"]["labels"]
280
+ X_val = ds["validation"]["text"]; y_val = ds["validation"]["labels"]
 
281
 
 
282
  mlb = MultiLabelBinarizer(classes=list(range(len(label_names))))
283
+ Y_train = mlb.fit_transform(y_train)
284
+ Y_val = mlb.transform(y_val)
285
+
286
+ clf = Pipeline([
287
+ ("tfidf", TfidfVectorizer(lowercase=True, ngram_range=(1, 2), min_df=2, max_df=0.9, strip_accents="unicode")),
288
+ ("ovr", OneVsRestClassifier(LogisticRegression(solver="saga", max_iter=1000, n_jobs=-1, class_weight="balanced"), n_jobs=-1))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  ])
290
 
291
+ print("[MM] Training classifier...")
292
  clf.fit(X_train, Y_train)
293
 
294
+ print(f"[MM] Validation macro F1: {f1_score(Y_val, clf.predict(X_val), average='macro', zero_division=0):.3f}")
 
 
 
 
 
 
 
 
 
 
 
 
295
 
296
+ joblib.dump({"version": MODEL_VERSION, "pipeline": clf, "mlb": mlb, "label_names": label_names}, MODEL_PATH)
297
  return clf, mlb, label_names
298
 
 
299
  try:
300
  CLASSIFIER, MLB, LABEL_NAMES = train_or_load_model()
301
  except Exception as e:
302
  print(f"[WARN] Failed to train/load classifier: {e}")
303
  CLASSIFIER, MLB, LABEL_NAMES = None, None, None
304
 
305
+ # ---------------- Emotion detection ----------------
306
  def classify_text(text: str):
307
+ if not CLASSIFIER: return []
 
 
 
 
 
 
308
  try:
309
  proba = CLASSIFIER.predict_proba([text])[0]
310
  except AttributeError:
 
 
311
  from scipy.special import expit
312
+ proba = expit(CLASSIFIER.decision_function([text])[0])
 
 
313
  idxs = [i for i, p in enumerate(proba) if p >= THRESHOLD]
 
314
  idxs.sort(key=lambda i: proba[i], reverse=True)
315
  return [(LABEL_NAMES[i], float(proba[i])) for i in idxs]
316
 
317
  def detect_emotions(text: str):
318
  chosen = classify_text(text)
319
+ if not chosen: return "neutral"
 
 
320
  bucket = {}
321
  for label, p in chosen:
322
  app = GOEMO_TO_APP.get(label.lower(), "neutral")
323
  bucket[app] = max(bucket.get(app, 0.0), p)
324
+ return max(bucket, key=bucket.get)
 
325
 
326
+ # ---------------- Reply composer ----------------
327
  def compose_support_legacy(main_emotion: str, is_first_msg: bool) -> str:
328
+ tip = random.choice(SUGGESTIONS.get(main_emotion, ["Take a slow breath. One small act of kindness can shift your day."]))
329
+ quote = random.choice(QUOTES.get(main_emotion, ["“No matter what you feel right now, this moment will pass.”"]))
330
+ include_quote = random.random() < 0.5
331
+
332
+ reply = tip
333
+ if include_quote:
334
+ reply += f"\n\n💬 {quote}"
 
 
 
 
 
 
 
 
 
 
335
 
336
  if is_first_msg:
337
  reply += "\n\n*Can you tell me a bit more about what’s behind that feeling?*"
 
341
  # ---------------- Chat logic ----------------
342
  def crisis_block(country):
343
  msg = CRISIS_NUMBERS.get(country, CRISIS_NUMBERS["Other / Not listed"])
344
+ return ("💛 I'm really sorry you're feeling like this. You matter.\n\n"
345
+ f"**If you might be in danger or thinking about harming yourself:**\n{msg}\n\n"
346
+ "Please reach out to someone now. You are not alone.")
 
 
347
 
348
  def chat_step(message, history, country, save_session):
349
  if CRISIS_RE.search(message):
350
  return crisis_block(country), "#FFD6E7"
 
351
  if CLOSING_RE.search(message):
352
  return ("Thank you 💛 Take care of yourself. Small steps matter. 🌿", "#FFFFFF")
353
 
354
+ emotion = detect_emotions(" ".join