cassandrasestier commited on
Commit
191af43
·
verified ·
1 Parent(s): 7b615b8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +49 -345
app.py CHANGED
@@ -1,10 +1,9 @@
1
  # ================================
2
  # 🪞 MoodMirror+ — Conversational Emotional Self-Care
3
  # Dataset-only: TF-IDF + OneVsRest Logistic Regression on GoEmotions
4
- # - Persists model & SQLite DB to /data (enable Persistent storage on HF Spaces)
5
- # - Always give at least one advice tip; sometimes add a quote
6
- # - Implicit emotion recognition (emoji/slang/negations hints)
7
- # - Dynamic threshold + Top-1 fallback
8
  # ================================
9
  import os
10
  import re
@@ -32,13 +31,13 @@ DATA_DIR = os.getenv("MM_DATA_DIR", _pick_data_dir())
32
  os.makedirs(DATA_DIR, exist_ok=True)
33
  DB_PATH = os.path.join(DATA_DIR, "moodmirror.db")
34
  MODEL_PATH = os.path.join(DATA_DIR, "goemo_sklearn.joblib")
35
- MODEL_VERSION = "v2-tfidf-lr-ovr-implicit" # bump when training recipe changes
36
 
37
- print(f"[MM] Using data dir: {DATA_DIR}")
38
- print(f"[MM] SQLite path: {DB_PATH}")
39
- print(f"[MM] Model path: {MODEL_PATH}")
40
 
41
- # ---------------- Crisis & regex ----------------
42
  CRISIS_RE = re.compile(
43
  r"\b(self[- ]?harm|suicid|kill myself|end my life|overdose|cutting|i don.?t want to live|can.?t go on)\b",
44
  re.I,
@@ -63,70 +62,49 @@ SUGGESTIONS = {
63
  "Be gentle with yourself. Cry if you need to — that’s healing, not weakness.",
64
  "Let yourself rest. You don’t have to be productive while your heart is heavy.",
65
  "Hold a pillow or blanket close and breathe slowly.",
66
- "Talk to someone who feels safe, or write down what you wish someone would say to you.",
67
  "It’s okay if today you just get through the day.",
68
- "Take a warm shower, drink water, and do one small thing that feels kind.",
69
- "Remember: sadness is a visitor, not your home.",
70
  "Even quiet tears are a form of strength — you’re still here.",
 
71
  ],
72
  "fear": [
73
  "Ground yourself with slow breathing — feel your feet on the floor.",
74
  "You are safe in this moment. Focus on what is real right now.",
75
- "Courage isn’t about not being afraid; it’s choosing to keep going anyway.",
76
  "Remind yourself: not every thought is a fact.",
77
- "Small steps are still progress you dont need to have the whole plan.",
78
- "Try saying out loud: ‘I can handle this one moment at a time.’",
79
  "Place a hand on your chest and whisper, ‘I am safe. I am present.’",
80
- "Fear often shows up before growth maybe something new is calling.",
81
  ],
82
  "joy": [
83
  "Savor this — your body deserves to feel good.",
84
- "Let the joy sink in; you don’t have to rush to the next thing.",
85
  "Share your happiness with someone — even a smile counts.",
86
  "Write down what’s bringing you joy today. It strengthens the memory.",
87
- "Take a deep breath and feel gratitude for this small light moment.",
88
  "Joy can be quiet — a cup of tea, a laugh, a calm pause.",
89
- "Smile for no reason; your brain will thank you later.",
90
- "Notice how joy feels in your body, and let yourself stay there for a while.",
91
  ],
92
  "anger": [
93
  "Pause and breathe — you don’t have to react right away.",
94
  "Anger can mean something important needs attention — listen kindly.",
95
  "Take a walk or shake out the tension before speaking.",
96
- "Write everything you want to say, then decide what actually needs saying.",
97
  "It’s okay to be angry — what matters is what you do with it.",
98
- "Drink water and give your body a few minutes to settle.",
99
- "Sometimes the kindest thing is to wait before responding.",
100
  "Ask yourself: ‘What boundary of mine needs care right now?’",
101
  ],
102
  "boredom": [
 
103
  "Boredom can be a doorway — something in you wants to wake up.",
104
- "Try something completely small and new: a song, a stretch, a random fact.",
105
- "Put on music and tidy for 5 minutes movement often sparks motivation.",
106
- "Write a list of ‘tiny adventures’ you could do today.",
107
- "Sometimes doing nothing is what your mind needs — rest counts.",
108
- "Look outside for a moment — notice one color or sound you hadn’t before.",
109
- "Ask yourself: ‘What might make me curious right now?’",
110
- "Do one creative thing without judging it — draw, hum, doodle, move.",
111
  ],
112
  "grief": [
113
  "Grief is love that has nowhere to go — let it speak softly through you.",
114
  "You’re allowed to miss them and still keep living.",
115
- "Light a candle, whisper their name, and breathe.",
116
  "Eat something, drink water — caring for your body honors their memory too.",
117
- "It’s okay to cry about the same thing more than once.",
118
- "Talk about your loss. The pain lessens when it’s witnessed.",
119
- "Hold an item that reminds you of love, not loss.",
120
  "Healing doesn’t mean forgetting; it means remembering with more peace.",
121
  ],
122
  "love": [
123
  "Tell someone you care about them — even in a small text.",
124
  "Give love back to yourself: you deserve your own gentleness.",
125
  "Love is quiet presence — you don’t have to prove it.",
126
- "A small act of kindness can brighten two days at once.",
127
- "You are loved simply for being, not for doing.",
128
- "Love often grows in silence and simple gestures.",
129
- "Send out a silent thank-you to those who care for you.",
130
  "Let yourself receive love — it’s not selfish to need warmth.",
131
  ],
132
  "nervousness": [
@@ -134,40 +112,24 @@ SUGGESTIONS = {
134
  "Relax your jaw, drop your shoulders, exhale longer than you inhale.",
135
  "You don’t have to fix every thought; let them pass like clouds.",
136
  "Try naming five things you can see right now.",
137
- "Anxiety is a false alarm — you can remind your brain you’re safe.",
138
- "Put both feet flat on the floor and press gently to ground yourself.",
139
- "Write down what worries you, then cross out what you can’t control.",
140
- "You can pause; you’re not behind. Safety starts with one breath.",
141
  ],
142
  "curiosity": [
143
  "Follow the spark — it doesn’t have to make sense yet.",
144
  "Ask the question that feels exciting, not the one that feels safe.",
145
- "Let yourself explore something simply because it’s interesting.",
146
  "Learning is self-care too — wonder is nourishment.",
147
- "Take 10 minutes to google something random you love.",
148
- "Curiosity is courage in disguise — let it lead.",
149
  "Even small discoveries can wake up your energy.",
150
- "Be a beginner; it’s a powerful place to start.",
151
  ],
152
  "gratitude": [
153
  "Name three things you’re grateful for — big or tiny.",
154
- "Gratitude turns ordinary moments into something sacred.",
155
- "Say ‘thank you’ — out loud, even if just to yourself.",
156
  "Look around and notice something that quietly supports you.",
157
  "Each breath is a gift you didn’t have to earn.",
158
  "Gratitude softens fear; it reminds you what’s still good.",
159
- "Send a short thank-you message to someone right now.",
160
- "Appreciate how far you’ve already come — quietly, just for you.",
161
  ],
162
  "neutral": [
163
  "Take one slow, deep breath. That’s a start.",
164
  "Not every moment has to be meaningful — existing is enough.",
165
  "Sometimes calm feels empty because we’re used to noise — rest in it.",
166
- "Stretch for 30 seconds and notice your body waking up.",
167
  "Drink water; your brain loves that.",
168
- "Sit still for one minute — that’s all you need.",
169
- "Neutral moments are where balance grows.",
170
- "Doing nothing for a while is still doing something.",
171
  ],
172
  }
173
 
@@ -175,50 +137,26 @@ QUOTES = {
175
  "sadness": [
176
  "“Even the darkest night will end and the sun will rise.” – Victor Hugo",
177
  "“You have survived every hard day so far.”",
178
- "“You don’t have to feel better to start healing.”",
179
  ],
180
  "fear": [
181
  "“Feel the fear and do it anyway.” – Susan Jeffers",
182
  "“This moment will not last forever.”",
183
- "“You’ve faced hard things before — you can again.”",
184
  ],
185
  "joy": [
186
  "“Happiness is not out there, it’s in you.”",
187
- "“Let joy be your rebellion.”",
188
  "“Enjoy the little things — one day you’ll realize they were the big things.”",
189
  ],
190
  "anger": [
191
  "“Peace begins with a pause.”",
192
  "“Anger is energy — guide it, don’t suppress it.”",
193
  ],
194
- "boredom": [
195
- "“Boredom is the beginning of imagination.” Jules Renard",
196
- "“Curiosity is the cure for boredom.” – Dorothy Parker",
197
- ],
198
- "grief": [
199
- "“Grief is love that has nowhere to go.”",
200
- "“Love doesn’t end, it changes form.”",
201
- ],
202
- "love": [
203
- "“Where there is love, there is life.” – Mahatma Gandhi",
204
- "“You are loved just for being who you are.” – Ram Dass",
205
- ],
206
- "nervousness": [
207
- "“Breathe. You are doing enough.”",
208
- "“This worry does not define you.”",
209
- ],
210
- "curiosity": [
211
- "“Stay curious — it’s the mind’s way of loving life.”",
212
- "“Every question plants a seed.”",
213
- ],
214
- "gratitude": [
215
- "“Gratitude turns what we have into enough.”",
216
- "“Thankfulness unlocks joy.”",
217
- ],
218
- "neutral": [
219
- "“Be present — even a calm moment can be a quiet victory.”",
220
- "“Peace is not the absence of chaos, but the presence of inner calm.”",
221
- ],
222
  }
223
 
224
  COLOR_MAP = {
@@ -240,106 +178,54 @@ GOEMO_TO_APP = {
240
  "sadness": "sadness", "surprise": "neutral", "neutral": "neutral",
241
  }
242
 
243
- # --- Threshold & implicit-emotion controls ---
244
- THRESHOLD = 0.30 # standard selection threshold
245
- MIN_THRESHOLD = 0.12 # floor if model is unsure
246
- TOP1_FALLBACK = True # ensure at least one label if nothing passes threshold
247
-
248
- # --- Implicit cues (emojis, slang, negations, intensity) ---
249
- EMOJI_HINTS = {
250
- "😢": "sadness", "😭": "sadness", "😞": "sadness", "💔": "grief",
251
- "😡": "anger", "🤬": "anger", "😤": "anger",
252
- "😱": "fear", "😨": "fear", "😰": "fear",
253
- "😌": "relief", "🙂": "joy", "😊": "joy", "😄": "joy", "😍": "love",
254
- "🤗": "love", "💖": "love", "🙏": "gratitude",
255
- "💤": "boredom", "🥱": "boredom", "🤔": "curiosity",
256
- "😐": "neutral", "😶": "neutral"
257
- }
258
- SLANG_HINTS = {
259
- "idk": "confusion", "meh": "boredom", "ugh": "annoyance", "nah": "disapproval",
260
- "wtf": "anger", "omg": "surprise", "lol": "amusement", "lmao": "amusement",
261
- "miss you": "grief", "miss her": "grief", "miss him": "grief",
262
- "im fine": "sadness", "i'm fine": "sadness"
263
- }
264
- NEGATION_PATTERNS = [
265
- ("not happy", "sadness"),
266
- ("not okay", "sadness"),
267
- ("not ok", "sadness"),
268
- ("not fine", "sadness"),
269
- ("no hope", "sadness"),
270
- ("no energy", "sadness"),
271
- ("no motivation", "boredom"),
272
- ("not safe", "fear"),
273
- ("not calm", "nervousness"),
274
- ("not sure", "confusion"),
275
- ]
276
- INTENSIFIERS = ["!!", "!!!", "?!", "?!?", "soooo", "very", "really", "super", "extremely"]
277
 
278
- def _lower(s: str) -> str:
279
- return s.lower() if isinstance(s, str) else ""
 
 
 
280
 
281
  def augment_text_for_classifier(text: str) -> str:
282
- """
283
- Inject 'hint tokens' based on implicit cues (emojis/slang/negations/intensity)
284
- to help TF-IDF pick up emotions not explicitly named.
285
- """
286
- if not text:
287
- return text
288
- t = _lower(text)
289
  hints = []
290
-
291
- # emojis
292
  for ch in text:
293
  if ch in EMOJI_HINTS:
294
  hints.append(EMOJI_HINTS[ch])
295
-
296
- # slang
297
  for k, v in SLANG_HINTS.items():
298
  if k in t:
299
  hints.append(v)
300
-
301
- # common negations
302
  for pat, lab in NEGATION_PATTERNS:
303
  if pat in t:
304
  hints.append(lab)
305
-
306
- # intensity: duplicate hints to weigh more
307
  if hints and any(x in t for x in INTENSIFIERS):
308
- hints = hints + hints
309
-
310
  if hints:
311
- hint_tokens = " ".join([f"emo_{h}" for h in hints])
312
- return text + " " + hint_tokens
313
  return text
314
 
315
- # ---------------- SQLite helpers ----------------
316
  def get_conn():
317
  return sqlite3.connect(DB_PATH, check_same_thread=False, timeout=10)
318
 
319
  def init_db():
320
  conn = get_conn()
321
  c = conn.cursor()
322
- c.execute("""
323
- CREATE TABLE IF NOT EXISTS sessions(
324
  id INTEGER PRIMARY KEY AUTOINCREMENT,
325
- ts TEXT,
326
- country TEXT,
327
- user_text TEXT,
328
- main_emotion TEXT
329
- )
330
- """)
331
- conn.commit()
332
- conn.close()
333
 
334
  def log_session(country, msg, emotion):
335
- conn = get_conn()
336
- c = conn.cursor()
337
- c.execute("INSERT INTO sessions(ts, country, user_text, main_emotion) VALUES(?,?,?,?)",
338
  (datetime.utcnow().isoformat(timespec="seconds"), country, msg[:500], emotion))
339
- conn.commit()
340
- conn.close()
341
 
342
- # ---------------- Train / Load model ----------------
343
  def load_goemotions_dataset():
344
  ds = load_dataset("google-research-datasets/go_emotions", "simplified")
345
  label_names = ds["train"].features["labels"].feature.names
@@ -347,195 +233,13 @@ def load_goemotions_dataset():
347
 
348
  def train_or_load_model():
349
  if os.path.isfile(MODEL_PATH):
350
- print("[MM] Loading cached classifier...")
351
  bundle = joblib.load(MODEL_PATH)
352
  if bundle.get("version") == MODEL_VERSION:
353
  return bundle["pipeline"], bundle["mlb"], bundle["label_names"]
354
-
355
- print("[MM] Loading GoEmotions dataset...")
356
- ds, label_names = load_goemotions_dataset()
357
-
358
- X_train = ds["train"]["text"]; y_train = ds["train"]["labels"]
359
- X_val = ds["validation"]["text"]; y_val = ds["validation"]["labels"]
360
-
361
- mlb = MultiLabelBinarizer(classes=list(range(len(label_names))))
362
- Y_train = mlb.fit_transform(y_train)
363
- Y_val = mlb.transform(y_val)
364
-
365
  clf = Pipeline([
366
- ("tfidf", TfidfVectorizer(
367
- lowercase=True, ngram_range=(1, 2), min_df=2, max_df=0.9, strip_accents="unicode"
368
- )),
369
- ("ovr", OneVsRestClassifier(
370
- LogisticRegression(solver="saga", max_iter=1000, n_jobs=-1, class_weight="balanced"),
371
- n_jobs=-1
372
- ))
373
- ])
374
-
375
- print("[MM] Training classifier...")
376
- clf.fit(X_train, Y_train)
377
- macro_f1 = f1_score(Y_val, clf.predict(X_val), average="macro", zero_division=0)
378
- print(f"[MM] Validation macro F1: {macro_f1:.3f}")
379
-
380
- joblib.dump({"version": MODEL_VERSION, "pipeline": clf, "mlb": mlb, "label_names": label_names}, MODEL_PATH)
381
- print(f"[MM] Saved model -> {MODEL_PATH}")
382
- return clf, mlb, label_names
383
-
384
- try:
385
- CLASSIFIER, MLB, LABEL_NAMES = train_or_load_model()
386
- except Exception as e:
387
- print(f"[WARN] Failed to train/load classifier: {e}")
388
- CLASSIFIER, MLB, LABEL_NAMES = None, None, None
389
-
390
- # ---------------- Emotion detection ----------------
391
- def classify_text(text: str):
392
- """
393
- Returns [(label_name, prob), ...] sorted desc.
394
- - Augments input with implicit-cue tokens
395
- - Uses dynamic threshold with a safety floor
396
- - Top-1 fallback ensures at least one label
397
- """
398
- if not CLASSIFIER or not LABEL_NAMES:
399
- return []
400
-
401
- augmented = augment_text_for_classifier(text)
402
-
403
- try:
404
- proba = CLASSIFIER.predict_proba([augmented])[0]
405
- except AttributeError:
406
- from scipy.special import expit
407
- scores = CLASSIFIER.decision_function([augmented])[0]
408
- proba = expit(scores)
409
-
410
- # Dynamic threshold
411
- maxp = float(max(proba)) if len(proba) else 0.0
412
- thr = THRESHOLD if maxp >= THRESHOLD else max(MIN_THRESHOLD, maxp * 0.8)
413
-
414
- idxs = [i for i, p in enumerate(proba) if p >= thr]
415
- idxs.sort(key=lambda i: proba[i], reverse=True)
416
-
417
- # Top-1 fallback
418
- if not idxs and TOP1_FALLBACK and len(proba):
419
- top1 = int(max(range(len(proba)), key=lambda i: proba[i]))
420
- idxs = [top1]
421
-
422
- return [(LABEL_NAMES[i], float(proba[i])) for i in idxs]
423
-
424
- def detect_emotions(text: str):
425
- chosen = classify_text(text)
426
- if not chosen:
427
- return "neutral"
428
- bucket = {}
429
- for label, p in chosen:
430
- app = GOEMO_TO_APP.get(label.lower(), "neutral")
431
- bucket[app] = max(bucket.get(app, 0.0), p)
432
- return max(bucket, key=bucket.get) if bucket else "neutral"
433
-
434
- # ---------------- Reply composer ----------------
435
- def compose_support(main_emotion: str, is_first_msg: bool) -> str:
436
- # Always include an advice tip; 50% chance to add a quote
437
- tip = random.choice(SUGGESTIONS.get(
438
- main_emotion,
439
- ["Take a slow breath. One small act of kindness can shift your day."]
440
- ))
441
- quote = random.choice(QUOTES.get(
442
- main_emotion,
443
- ["“No matter what you feel right now, this moment will pass.”"]
444
- ))
445
- include_quote = random.random() < 0.5
446
-
447
- reply = tip
448
- if include_quote:
449
- reply += f"\n\n💬 {quote}"
450
-
451
- if is_first_msg:
452
- reply += "\n\n*Can you tell me a bit more about what’s behind that feeling?*"
453
-
454
- return reply
455
-
456
- # ---------------- Chat logic ----------------
457
- def crisis_block(country):
458
- msg = CRISIS_NUMBERS.get(country, CRISIS_NUMBERS["Other / Not listed"])
459
- return ("💛 I'm really sorry you're feeling like this. You matter.\n\n"
460
- f"**If you might be in danger or thinking about harming yourself:**\n{msg}\n\n"
461
- "Please reach out to someone now. You are not alone.")
462
-
463
- def chat_step(message, history, country, save_session):
464
- if CRISIS_RE.search(message):
465
- return crisis_block(country), "#FFD6E7"
466
- if CLOSING_RE.search(message):
467
- return ("Thank you 💛 Take care of yourself. Small steps matter. 🌿", "#FFFFFF")
468
-
469
- recent = " ".join(message.split()[-100:])
470
- emotion = detect_emotions(recent)
471
- color = COLOR_MAP.get(emotion, "#FFFFFF")
472
-
473
- if save_session:
474
- log_session(country, message, emotion)
475
-
476
- reply = compose_support(emotion, is_first_msg=not bool(history))
477
- return reply, color
478
-
479
- # ---------------- Gradio UI ----------------
480
- init_db()
481
-
482
- custom_css = """
483
- :root, body, .gradio-container { transition: background-color 0.8s ease !important; }
484
- .typing { font-style: italic; opacity: 0.8; animation: blink 1s infinite; }
485
- @keyframes blink { 50% {opacity: 0.4;} }
486
- """
487
-
488
- with gr.Blocks(css=custom_css, title="🪞 MoodMirror+ (Dataset-only Edition)") as demo:
489
- style_injector = gr.HTML("")
490
- gr.Markdown(
491
- "### 🪞 MoodMirror+ — Emotional Support & Inspiration 🌸\n"
492
- "Powered only by the **GoEmotions dataset** (trained locally on startup).\n\n"
493
- "_Not medical advice. If you feel unsafe, please reach out for help immediately._"
494
- )
495
-
496
- with gr.Row():
497
- country = gr.Dropdown(choices=list(CRISIS_NUMBERS.keys()),
498
- value="Other / Not listed", label="Country")
499
- save_ok = gr.Checkbox(value=False, label="Save anonymized session (no personal data)")
500
-
501
- chat = gr.Chatbot(height=360)
502
- msg = gr.Textbox(placeholder="Type how you feel...", label="Your message")
503
- send = gr.Button("Send")
504
- typing = gr.Markdown("", elem_classes="typing")
505
-
506
- # Optional: dataset preview
507
- with gr.Accordion("🔎 Preview GoEmotions samples", open=False):
508
- with gr.Row():
509
- n_examples = gr.Slider(1, 10, value=5, step=1, label="Number of examples")
510
- split = gr.Dropdown(["train", "validation", "test"], value="train", label="Split")
511
- refresh = gr.Button("Show samples")
512
- table = gr.Dataframe(headers=["text", "labels"], row_count=5, wrap=True)
513
-
514
- def refresh_samples(n, split_name):
515
- try:
516
- ds = load_dataset("google-research-datasets/go_emotions", "simplified")
517
- names = ds["train"].features["labels"].feature.names
518
- rows = ds[split_name].shuffle(seed=42).select(range(min(int(n), len(ds[split_name]))))
519
- return [[t, ", ".join([names[i] for i in labs])] for t, labs in zip(rows["text"], rows["labels"])]
520
- except Exception as e:
521
- return [[f"Dataset load error: {e}", ""]]
522
-
523
- refresh.click(refresh_samples, inputs=[n_examples, split], outputs=[table])
524
-
525
- def respond(user_msg, chat_hist, country_choice, save_flag):
526
- if not user_msg or not user_msg.strip():
527
- yield chat_hist + [[user_msg, "Please share a short sentence about how you feel 🙂"]], "", "", ""
528
- return
529
- yield chat_hist, "💭 MoodMirror is thinking...", "", ""
530
- reply, color = chat_step(user_msg, chat_hist, country_choice, bool(save_flag))
531
- style_tag = f"<style>:root,body,.gradio-container{{background:{color}!important;}}</style>"
532
- yield chat_hist + [[user_msg, reply]], "", style_tag, ""
533
-
534
- send.click(respond, inputs=[msg, chat, country, save_ok],
535
- outputs=[chat, typing, style_injector, msg], queue=True)
536
- msg.submit(respond, inputs=[msg, chat, country, save_ok],
537
- outputs=[chat, typing, style_injector, msg], queue=True)
538
-
539
- if __name__ == "__main__":
540
- demo.queue()
541
- demo.launch()
 
1
  # ================================
2
  # 🪞 MoodMirror+ — Conversational Emotional Self-Care
3
  # Dataset-only: TF-IDF + OneVsRest Logistic Regression on GoEmotions
4
+ # - Persists model & SQLite DB to /data (HF Spaces persistent storage)
5
+ # - Always gives an advice tip (plus sometimes a quote)
6
+ # - Detects implicit emotion cues (emoji/slang/negation)
 
7
  # ================================
8
  import os
9
  import re
 
31
  os.makedirs(DATA_DIR, exist_ok=True)
32
  DB_PATH = os.path.join(DATA_DIR, "moodmirror.db")
33
  MODEL_PATH = os.path.join(DATA_DIR, "goemo_sklearn.joblib")
34
+ MODEL_VERSION = "v3-always-tip"
35
 
36
+ print(f"[MM] Data dir: {DATA_DIR}")
37
+ print(f"[MM] DB path: {DB_PATH}")
38
+ print(f"[MM] Model: {MODEL_PATH}")
39
 
40
+ # ---------------- Regex & Crisis ----------------
41
  CRISIS_RE = re.compile(
42
  r"\b(self[- ]?harm|suicid|kill myself|end my life|overdose|cutting|i don.?t want to live|can.?t go on)\b",
43
  re.I,
 
62
  "Be gentle with yourself. Cry if you need to — that’s healing, not weakness.",
63
  "Let yourself rest. You don’t have to be productive while your heart is heavy.",
64
  "Hold a pillow or blanket close and breathe slowly.",
 
65
  "It’s okay if today you just get through the day.",
66
+ "Talk to someone who feels safe, or write down what you wish someone would say to you.",
 
67
  "Even quiet tears are a form of strength — you’re still here.",
68
+ "Take a warm shower, drink water, and do one small thing that feels kind.",
69
  ],
70
  "fear": [
71
  "Ground yourself with slow breathing — feel your feet on the floor.",
72
  "You are safe in this moment. Focus on what is real right now.",
 
73
  "Remind yourself: not every thought is a fact.",
74
+ "Courage isn’t the absence of fear, its choosing to go on anyway.",
 
75
  "Place a hand on your chest and whisper, ‘I am safe. I am present.’",
76
+ "Try saying: ‘I can handle this one moment at a time.’",
77
  ],
78
  "joy": [
79
  "Savor this — your body deserves to feel good.",
 
80
  "Share your happiness with someone — even a smile counts.",
81
  "Write down what’s bringing you joy today. It strengthens the memory.",
 
82
  "Joy can be quiet — a cup of tea, a laugh, a calm pause.",
83
+ "Let the joy sink in; you don’t have to rush to the next thing.",
 
84
  ],
85
  "anger": [
86
  "Pause and breathe — you don’t have to react right away.",
87
  "Anger can mean something important needs attention — listen kindly.",
88
  "Take a walk or shake out the tension before speaking.",
 
89
  "It’s okay to be angry — what matters is what you do with it.",
 
 
90
  "Ask yourself: ‘What boundary of mine needs care right now?’",
91
  ],
92
  "boredom": [
93
+ "Try something small and new: a song, a stretch, a random fact.",
94
  "Boredom can be a doorway — something in you wants to wake up.",
95
+ "Write a list of tiny adventures you could do today.",
96
+ "Look outside and notice one color or sound you hadn’t before.",
 
 
 
 
 
97
  ],
98
  "grief": [
99
  "Grief is love that has nowhere to go — let it speak softly through you.",
100
  "You’re allowed to miss them and still keep living.",
 
101
  "Eat something, drink water — caring for your body honors their memory too.",
 
 
 
102
  "Healing doesn’t mean forgetting; it means remembering with more peace.",
103
  ],
104
  "love": [
105
  "Tell someone you care about them — even in a small text.",
106
  "Give love back to yourself: you deserve your own gentleness.",
107
  "Love is quiet presence — you don’t have to prove it.",
 
 
 
 
108
  "Let yourself receive love — it’s not selfish to need warmth.",
109
  ],
110
  "nervousness": [
 
112
  "Relax your jaw, drop your shoulders, exhale longer than you inhale.",
113
  "You don’t have to fix every thought; let them pass like clouds.",
114
  "Try naming five things you can see right now.",
 
 
 
 
115
  ],
116
  "curiosity": [
117
  "Follow the spark — it doesn’t have to make sense yet.",
118
  "Ask the question that feels exciting, not the one that feels safe.",
 
119
  "Learning is self-care too — wonder is nourishment.",
 
 
120
  "Even small discoveries can wake up your energy.",
 
121
  ],
122
  "gratitude": [
123
  "Name three things you’re grateful for — big or tiny.",
 
 
124
  "Look around and notice something that quietly supports you.",
125
  "Each breath is a gift you didn’t have to earn.",
126
  "Gratitude softens fear; it reminds you what’s still good.",
 
 
127
  ],
128
  "neutral": [
129
  "Take one slow, deep breath. That’s a start.",
130
  "Not every moment has to be meaningful — existing is enough.",
131
  "Sometimes calm feels empty because we’re used to noise — rest in it.",
 
132
  "Drink water; your brain loves that.",
 
 
 
133
  ],
134
  }
135
 
 
137
  "sadness": [
138
  "“Even the darkest night will end and the sun will rise.” – Victor Hugo",
139
  "“You have survived every hard day so far.”",
 
140
  ],
141
  "fear": [
142
  "“Feel the fear and do it anyway.” – Susan Jeffers",
143
  "“This moment will not last forever.”",
 
144
  ],
145
  "joy": [
146
  "“Happiness is not out there, it’s in you.”",
 
147
  "“Enjoy the little things — one day you’ll realize they were the big things.”",
148
  ],
149
  "anger": [
150
  "“Peace begins with a pause.”",
151
  "“Anger is energy — guide it, don’t suppress it.”",
152
  ],
153
+ "boredom": ["“Curiosity is the cure for boredom.” – Dorothy Parker"],
154
+ "grief": ["Grief is love that has nowhere to go.”"],
155
+ "love": ["Where there is love, there is life.” – Mahatma Gandhi"],
156
+ "nervousness": ["“Breathe. You are doing enough.”"],
157
+ "curiosity": ["“Stay curious — it’s the mind’s way of loving life.”"],
158
+ "gratitude": ["Gratitude turns what we have into enough.”"],
159
+ "neutral": ["Be present even a calm moment can be a quiet victory.”"],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  }
161
 
162
  COLOR_MAP = {
 
178
  "sadness": "sadness", "surprise": "neutral", "neutral": "neutral",
179
  }
180
 
181
+ # --- Emotion detection parameters ---
182
+ THRESHOLD = 0.30
183
+ MIN_THRESHOLD = 0.12
184
+ TOP1_FALLBACK = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
+ # --- Implicit cues ---
187
+ EMOJI_HINTS = {"😢": "sadness", "😭": "sadness", "😡": "anger", "😍": "love", "🤔": "curiosity"}
188
+ SLANG_HINTS = {"idk": "confusion", "meh": "boredom", "ugh": "annoyance", "im fine": "sadness"}
189
+ NEGATION_PATTERNS = [("not happy", "sadness"), ("not ok", "sadness"), ("not calm", "nervousness")]
190
+ INTENSIFIERS = ["!!", "!!!", "really", "so", "very"]
191
 
192
  def augment_text_for_classifier(text: str) -> str:
193
+ t = text.lower()
 
 
 
 
 
 
194
  hints = []
 
 
195
  for ch in text:
196
  if ch in EMOJI_HINTS:
197
  hints.append(EMOJI_HINTS[ch])
 
 
198
  for k, v in SLANG_HINTS.items():
199
  if k in t:
200
  hints.append(v)
 
 
201
  for pat, lab in NEGATION_PATTERNS:
202
  if pat in t:
203
  hints.append(lab)
 
 
204
  if hints and any(x in t for x in INTENSIFIERS):
205
+ hints += hints
 
206
  if hints:
207
+ return text + " " + " ".join([f"emo_{h}" for h in hints])
 
208
  return text
209
 
210
+ # ---------------- DB helpers ----------------
211
  def get_conn():
212
  return sqlite3.connect(DB_PATH, check_same_thread=False, timeout=10)
213
 
214
  def init_db():
215
  conn = get_conn()
216
  c = conn.cursor()
217
+ c.execute("""CREATE TABLE IF NOT EXISTS sessions(
 
218
  id INTEGER PRIMARY KEY AUTOINCREMENT,
219
+ ts TEXT, country TEXT, user_text TEXT, main_emotion TEXT)""")
220
+ conn.commit(); conn.close()
 
 
 
 
 
 
221
 
222
  def log_session(country, msg, emotion):
223
+ conn = get_conn(); c = conn.cursor()
224
+ c.execute("INSERT INTO sessions(ts,country,user_text,main_emotion)VALUES(?,?,?,?)",
 
225
  (datetime.utcnow().isoformat(timespec="seconds"), country, msg[:500], emotion))
226
+ conn.commit(); conn.close()
 
227
 
228
+ # ---------------- Train / Load ----------------
229
  def load_goemotions_dataset():
230
  ds = load_dataset("google-research-datasets/go_emotions", "simplified")
231
  label_names = ds["train"].features["labels"].feature.names
 
233
 
234
  def train_or_load_model():
235
  if os.path.isfile(MODEL_PATH):
 
236
  bundle = joblib.load(MODEL_PATH)
237
  if bundle.get("version") == MODEL_VERSION:
238
  return bundle["pipeline"], bundle["mlb"], bundle["label_names"]
239
+ ds, names = load_goemotions_dataset()
240
+ X_train, y_train = ds["train"]["text"], ds["train"]["labels"]
241
+ X_val, y_val = ds["validation"]["text"], ds["validation"]["labels"]
242
+ mlb = MultiLabelBinarizer(classes=list(range(len(names))))
243
+ Y_train, Y_val = mlb.fit_transform(y_train), mlb.transform(y_val)
 
 
 
 
 
 
244
  clf = Pipeline([
245
+ ("tfidf", TfidfVectorizer(lowercase=True, ngram_range=(1,2), min_df=2, max_df=0.9)),