cassandrasestier commited on
Commit
baef3a3
·
verified ·
1 Parent(s): 795a14d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +109 -206
app.py CHANGED
@@ -1,9 +1,6 @@
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,132 +28,96 @@ DATA_DIR = os.getenv("MM_DATA_DIR", _pick_data_dir())
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,
44
  )
45
  CLOSING_RE = re.compile(
46
- r"\b(thanks?|thank you|that'?s all|bye|goodbye|see you|take care|ok bye|no thanks?)\b",
47
  re.I,
48
  )
49
 
50
  CRISIS_NUMBERS = {
51
- "United States": "Call or text **988** (24/7 Suicide & Crisis Lifeline). If in immediate danger, call **911**.",
52
- "Canada": "Call or text **988** (Suicide Crisis Helpline, 24/7). If in immediate danger, call **911**.",
53
- "United Kingdom / ROI": "Call **116 123** (Samaritans, 24/7). If in immediate danger, call **999 / 112**.",
54
- "France": "Call **3114** (Numéro national de prévention du suicide, 24/7). If in immediate danger, call **112 / 15**.",
55
- "Australia": "Call **13 11 14** (Lifeline, 24/7). If in immediate danger, call **000**.",
56
- "Other / Not listed": "Call your local emergency number (**112/911**) or search suicide crisis hotline + your country.",
57
  }
58
 
59
- # ---------------- Advice & Quotes ----------------
60
  SUGGESTIONS = {
61
  "sadness": [
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, it’s 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": [
111
- "Breathe slower than your worry your body will follow your rhythm.",
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
-
136
- QUOTES = {
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,36 +139,30 @@ GOEMO_TO_APP = {
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", "i'm fine": "sadness"}
189
- NEGATION_PATTERNS = [("not happy", "sadness"), ("not ok", "sadness"), ("not calm", "nervousness"), ("no motivation","boredom")]
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
 
@@ -217,19 +172,21 @@ def init_db():
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
232
- return ds, label_names
233
 
234
  def train_or_load_model():
235
  if os.path.isfile(MODEL_PATH):
@@ -238,69 +195,48 @@ def train_or_load_model():
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, strip_accents="unicode")),
246
  ("ovr", OneVsRestClassifier(
247
- LogisticRegression(solver="saga", max_iter=1000, class_weight="balanced"),
248
- n_jobs=-1))
249
  ])
250
  clf.fit(X_train, Y_train)
251
- print(f"[MM] Val macro F1: {f1_score(Y_val, clf.predict(X_val), average='macro', zero_division=0):.3f}")
252
  joblib.dump({"version": MODEL_VERSION, "pipeline": clf, "mlb": mlb, "label_names": names}, MODEL_PATH)
253
  return clf, mlb, names
254
 
255
  try:
256
  CLASSIFIER, MLB, LABEL_NAMES = train_or_load_model()
257
  except Exception as e:
258
- print(f"[WARN] Model load error: {e}")
259
  CLASSIFIER, MLB, LABEL_NAMES = None, None, None
260
 
261
- # ---------------- Emotion inference ----------------
262
- def classify_text(text: str):
263
- if not CLASSIFIER or not LABEL_NAMES: return []
264
- augmented = augment_text_for_classifier(text)
265
- try:
266
- proba = CLASSIFIER.predict_proba([augmented])[0]
267
- except AttributeError:
268
- from scipy.special import expit
269
- proba = expit(CLASSIFIER.decision_function([augmented])[0])
270
- maxp = float(max(proba)) if len(proba) else 0.0
271
  thr = THRESHOLD if maxp >= THRESHOLD else max(MIN_THRESHOLD, maxp * 0.8)
272
- idxs = [i for i, p in enumerate(proba) if p >= thr]
273
- if not idxs and TOP1_FALLBACK and len(proba):
274
- idxs = [int(max(range(len(proba)), key=lambda i: proba[i]))]
275
  idxs.sort(key=lambda i: proba[i], reverse=True)
276
  return [(LABEL_NAMES[i], float(proba[i])) for i in idxs]
277
 
278
- def detect_emotions(text: str):
279
  chosen = classify_text(text)
280
  if not chosen: return "neutral"
281
  bucket = {}
282
- for label, p in chosen:
283
- app = GOEMO_TO_APP.get(label.lower(), "neutral")
284
- bucket[app] = max(bucket.get(app, 0.0), p)
285
  return max(bucket, key=bucket.get) if bucket else "neutral"
286
 
287
- # ---------------- Reply composer ----------------
288
- def compose_support(main_emotion: str, is_first_msg: bool) -> str:
289
- tip = random.choice(SUGGESTIONS.get(main_emotion, SUGGESTIONS["neutral"]))
290
- quote = random.choice(QUOTES.get(main_emotion, ["“This too shall pass.”"]))
291
- reply = f"• {tip}" # ALWAYS a tip
292
- if random.random() < 0.5:
293
- reply += f"\n\n💬 {quote}"
294
- if is_first_msg:
295
- reply += "\n\n*Can you tell me a bit more about what’s behind that feeling?*"
296
- return reply
297
-
298
  # ---------------- Chat logic ----------------
299
  def crisis_block(country):
300
  msg = CRISIS_NUMBERS.get(country, CRISIS_NUMBERS["Other / Not listed"])
301
- return ("💛 I'm really sorry you're feeling like this. You matter.\n\n"
302
- f"**If you might be in danger or thinking about harming yourself:**\n{msg}\n\n"
303
- "Please reach out to someone now. You are not alone.")
304
 
305
  def chat_step(message, history, country, save_session):
306
  if CRISIS_RE.search(message):
@@ -308,74 +244,41 @@ def chat_step(message, history, country, save_session):
308
  if CLOSING_RE.search(message):
309
  emotion = detect_emotions(message)
310
  tip = random.choice(SUGGESTIONS.get(emotion, SUGGESTIONS["neutral"]))
311
- return (f"Thank you 💛 One last gentle thought:\n\n• {tip}", "#FFFFFF")
312
-
313
- recent = " ".join(message.split()[-100:])
314
- emotion = detect_emotions(recent)
315
  color = COLOR_MAP.get(emotion, "#FFFFFF")
316
  if save_session:
317
  log_session(country, message, emotion)
318
- reply = compose_support(emotion, is_first_msg=not bool(history))
 
 
 
319
  return reply, color
320
 
321
- # ---------------- Gradio UI ----------------
322
  init_db()
323
 
324
- custom_css = """
325
- :root, body, .gradio-container { transition: background-color 0.8s ease !important; }
326
- .typing { font-style: italic; opacity: 0.8; animation: blink 1s infinite; }
327
- @keyframes blink { 50% {opacity: 0.4;} }
328
- """
329
-
330
- with gr.Blocks(css=custom_css, title="🪞 MoodMirror+ (Dataset-only Edition)") as demo:
331
- style_injector = gr.HTML("")
332
- gr.Markdown(
333
- "### 🪞 MoodMirror+ — Emotional Support & Inspiration 🌸\n"
334
- "Powered only by the **GoEmotions dataset** (trained locally on startup).\n\n"
335
- "_Not medical advice. If you feel unsafe, please reach out for help immediately._"
336
- )
337
-
338
  with gr.Row():
339
- country = gr.Dropdown(choices=list(CRISIS_NUMBERS.keys()),
340
- value="Other / Not listed", label="Country")
341
- save_ok = gr.Checkbox(value=False, label="Save anonymized session (no personal data)")
342
-
343
  chat = gr.Chatbot(height=360)
344
- msg = gr.Textbox(placeholder="Type how you feel...", label="Your message")
345
  send = gr.Button("Send")
346
- typing = gr.Markdown("", elem_classes="typing")
347
-
348
- # Optional: dataset preview
349
- with gr.Accordion("🔎 Preview GoEmotions samples", open=False):
350
- with gr.Row():
351
- n_examples = gr.Slider(1, 10, value=5, step=1, label="Number of examples")
352
- split = gr.Dropdown(["train", "validation", "test"], value="train", label="Split")
353
- refresh = gr.Button("Show samples")
354
- table = gr.Dataframe(headers=["text", "labels"], row_count=5, wrap=True)
355
-
356
- def refresh_samples(n, split_name):
357
- try:
358
- ds = load_dataset("google-research-datasets/go_emotions", "simplified")
359
- names = ds["train"].features["labels"].feature.names
360
- rows = ds[split_name].shuffle(seed=42).select(range(min(int(n), len(ds[split_name]))))
361
- return [[t, ", ".join([names[i] for i in labs])] for t, labs in zip(rows["text"], rows["labels"])]
362
- except Exception as e:
363
- return [[f"Dataset load error: {e}", ""]]
364
-
365
- refresh.click(refresh_samples, inputs=[n_examples, split], outputs=[table])
366
 
367
  def respond(user_msg, chat_hist, country_choice, save_flag):
368
- if not user_msg or not user_msg.strip():
369
- # outputs: [chat, typing, style_injector, msg]
370
  return chat_hist + [[user_msg, "Please share a short sentence about how you feel 🙂"]], "", "", ""
371
  reply, color = chat_step(user_msg, chat_hist, country_choice, bool(save_flag))
372
  style_tag = f"<style>:root,body,.gradio-container{{background:{color}!important;}}</style>"
373
  return chat_hist + [[user_msg, reply]], "", style_tag, ""
374
 
375
- send.click(respond, inputs=[msg, chat, country, save_ok],
376
- outputs=[chat, typing, style_injector, msg], queue=True)
377
- msg.submit(respond, inputs=[msg, chat, country, save_ok],
378
- outputs=[chat, typing, style_injector, msg], queue=True)
379
 
380
  if __name__ == "__main__":
381
  demo.queue()
 
1
  # ================================
2
  # 🪞 MoodMirror+ — Conversational Emotional Self-Care
3
+ # Dataset-only (GoEmotions) + always gives a self-care advice (no quotes)
 
 
 
4
  # ================================
5
  import os
6
  import re
 
28
  os.makedirs(DATA_DIR, exist_ok=True)
29
  DB_PATH = os.path.join(DATA_DIR, "moodmirror.db")
30
  MODEL_PATH = os.path.join(DATA_DIR, "goemo_sklearn.joblib")
31
+ MODEL_VERSION = "v4-advice-only"
32
 
33
+ # ---------------- Crisis detection ----------------
 
 
 
 
34
  CRISIS_RE = re.compile(
35
  r"\b(self[- ]?harm|suicid|kill myself|end my life|overdose|cutting|i don.?t want to live|can.?t go on)\b",
36
  re.I,
37
  )
38
  CLOSING_RE = re.compile(
39
+ r"\b(thanks?|thank you|bye|goodbye|see you|take care|ok bye|no thanks?)\b",
40
  re.I,
41
  )
42
 
43
  CRISIS_NUMBERS = {
44
+ "United States": "Call or text **988** (Suicide & Crisis Lifeline, 24/7).",
45
+ "France": "Call **3114** (Numéro national de prévention du suicide, 24/7).",
46
+ "United Kingdom / ROI": "Call **116 123** (Samaritans, 24/7).",
47
+ "Canada": "Call or text **988** (Suicide Crisis Helpline).",
48
+ "Australia": "Call **13 11 14** (Lifeline).",
49
+ "Other / Not listed": "Call your local emergency number (**112/911**) or search 'suicide hotline' in your country.",
50
  }
51
 
52
+ # ---------------- Advice library ----------------
53
  SUGGESTIONS = {
54
  "sadness": [
55
+ "Be gentle with yourself. Rest, cry, or connectit’s okay to feel low.",
56
+ "Let yourself breathe; you don’t have to fix everything today.",
57
+ "Try writing down what hurts and what you wish someone could say to you.",
58
+ "Drink water, open a window, or take a short walk — small acts help.",
59
+ "Remember, sadness passes more easily when you let it exist.",
 
 
60
  ],
61
  "fear": [
62
+ "Ground yourself name 5 things you see, 4 you feel, 3 you hear.",
63
+ "You are safe in this moment; focus on your breath.",
64
+ "Not every thought is a fact — notice which ones just want attention.",
65
+ "Take slow breaths; safety starts with one calm inhale.",
 
 
66
  ],
67
  "joy": [
68
+ "Let yourself smile and enjoy it fully.",
69
+ "Pause and notice how joy feels in your body.",
70
+ "Share a kind word or message happiness grows when shared.",
71
+ "Write down one thing that made you smile today.",
 
72
  ],
73
  "anger": [
74
+ "Pause before reacting; give yourself time to cool down.",
75
+ "Take deep breaths in through the nose, out slowly through the mouth.",
76
+ "Try walking or stretching to release the tension.",
77
+ "Ask yourself what boundary was crossed and how you can protect it calmly.",
 
78
  ],
79
  "boredom": [
80
+ "Try something small and new even a 2-minute change matters.",
81
+ "Move a little: tidy your space or step outside for fresh air.",
82
+ "Write down one creative idea, no matter how silly it feels.",
83
+ "Sometimes rest looks like boredom let it recharge you.",
84
  ],
85
  "grief": [
86
+ "Let the memories come it’s okay to cry or miss someone deeply.",
87
+ "Hold an object that reminds you of love, not loss.",
88
+ "Eat, drink water, and rest your body also grieves.",
89
+ "You don’t have to move on; you can move forward while remembering.",
90
  ],
91
  "love": [
92
+ "Reach out to someone you care about — a few words can mean a lot.",
93
+ "Take a deep breath and remind yourself that you are loved too.",
94
+ "Do one small act of kindness for yourself or another.",
95
+ "Love doesn’t have to be loud; quiet care counts too.",
96
  ],
97
  "nervousness": [
98
+ "Relax your shoulders, unclench your jaw, and breathe slowly.",
99
+ "Write your worries down, then cross out what you can’t control.",
100
+ "Try the 4-7-8 breath: inhale 4, hold 7, exhale 8.",
101
+ "Tell yourself: 'I can handle this one moment at a time.'",
102
  ],
103
  "curiosity": [
104
+ "Follow what interests you, even if it seems random.",
105
+ "Ask one new question today curiosity keeps your mind alive.",
106
+ "Try learning something small with no pressure to master it.",
107
+ "Explore a thought just because it feels interesting.",
108
  ],
109
  "gratitude": [
110
+ "Name three things you’re grateful for right now.",
111
+ "Say thank you even silently — for something that helped you.",
112
+ "Take a photo or note of something simple that brings comfort.",
113
+ "Remember that small joys count just as much as big ones.",
114
  ],
115
  "neutral": [
116
+ "Take a slow, conscious breath and relax your body.",
117
+ "Notice one pleasant detail around yousound, color, or scent.",
118
+ "Sit quietly for a minute; calm moments build strength.",
119
+ "Stretch or move — it helps your mood reset naturally.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  ],
 
 
 
 
 
 
 
121
  }
122
 
123
  COLOR_MAP = {
 
139
  "sadness": "sadness", "surprise": "neutral", "neutral": "neutral",
140
  }
141
 
142
+ # ---------------- Model configuration ----------------
143
+ THRESHOLD = 0.3
144
  MIN_THRESHOLD = 0.12
145
  TOP1_FALLBACK = True
146
 
147
+ # ---------------- Helper: augment text ----------------
148
  EMOJI_HINTS = {"😢": "sadness", "😭": "sadness", "😡": "anger", "😍": "love", "🤔": "curiosity"}
149
+ SLANG_HINTS = {"idk": "confusion", "meh": "boredom", "ugh": "annoyance", "im fine": "sadness"}
150
+ NEGATION_PATTERNS = [("not happy", "sadness"), ("not ok", "sadness"), ("no motivation", "boredom")]
 
151
 
152
+ def augment_text(text):
153
  t = text.lower()
154
  hints = []
155
  for ch in text:
156
+ if ch in EMOJI_HINTS: hints.append(EMOJI_HINTS[ch])
 
157
  for k, v in SLANG_HINTS.items():
158
+ if k in t: hints.append(v)
 
159
  for pat, lab in NEGATION_PATTERNS:
160
+ if pat in t: hints.append(lab)
 
 
 
161
  if hints:
162
  return text + " " + " ".join([f"emo_{h}" for h in hints])
163
  return text
164
 
165
+ # ---------------- DB setup ----------------
166
  def get_conn():
167
  return sqlite3.connect(DB_PATH, check_same_thread=False, timeout=10)
168
 
 
172
  c.execute("""CREATE TABLE IF NOT EXISTS sessions(
173
  id INTEGER PRIMARY KEY AUTOINCREMENT,
174
  ts TEXT, country TEXT, user_text TEXT, main_emotion TEXT)""")
175
+ conn.commit()
176
+ conn.close()
177
 
178
  def log_session(country, msg, emotion):
179
+ conn = get_conn()
180
+ c = conn.cursor()
181
  c.execute("INSERT INTO sessions(ts,country,user_text,main_emotion)VALUES(?,?,?,?)",
182
  (datetime.utcnow().isoformat(timespec="seconds"), country, msg[:500], emotion))
183
+ conn.commit()
184
+ conn.close()
185
 
186
+ # ---------------- Model training/loading ----------------
187
  def load_goemotions_dataset():
188
  ds = load_dataset("google-research-datasets/go_emotions", "simplified")
189
+ return ds, ds["train"].features["labels"].feature.names
 
190
 
191
  def train_or_load_model():
192
  if os.path.isfile(MODEL_PATH):
 
195
  return bundle["pipeline"], bundle["mlb"], bundle["label_names"]
196
  ds, names = load_goemotions_dataset()
197
  X_train, y_train = ds["train"]["text"], ds["train"]["labels"]
 
198
  mlb = MultiLabelBinarizer(classes=list(range(len(names))))
199
+ Y_train = mlb.fit_transform(y_train)
200
  clf = Pipeline([
201
+ ("tfidf", TfidfVectorizer(lowercase=True, ngram_range=(1,2), min_df=2, max_df=0.9)),
202
  ("ovr", OneVsRestClassifier(
203
+ LogisticRegression(solver="saga", max_iter=1000, class_weight="balanced"), n_jobs=-1))
 
204
  ])
205
  clf.fit(X_train, Y_train)
 
206
  joblib.dump({"version": MODEL_VERSION, "pipeline": clf, "mlb": mlb, "label_names": names}, MODEL_PATH)
207
  return clf, mlb, names
208
 
209
  try:
210
  CLASSIFIER, MLB, LABEL_NAMES = train_or_load_model()
211
  except Exception as e:
212
+ print("[WARN] Model not loaded:", e)
213
  CLASSIFIER, MLB, LABEL_NAMES = None, None, None
214
 
215
+ # ---------------- Emotion detection ----------------
216
+ def classify_text(text):
217
+ if not CLASSIFIER: return []
218
+ t = augment_text(text)
219
+ proba = CLASSIFIER.predict_proba([t])[0]
220
+ maxp = max(proba)
 
 
 
 
221
  thr = THRESHOLD if maxp >= THRESHOLD else max(MIN_THRESHOLD, maxp * 0.8)
222
+ idxs = [i for i, p in enumerate(proba) if p >= thr] or [int(proba.argmax())]
 
 
223
  idxs.sort(key=lambda i: proba[i], reverse=True)
224
  return [(LABEL_NAMES[i], float(proba[i])) for i in idxs]
225
 
226
+ def detect_emotions(text):
227
  chosen = classify_text(text)
228
  if not chosen: return "neutral"
229
  bucket = {}
230
+ for lbl, p in chosen:
231
+ app = GOEMO_TO_APP.get(lbl.lower(), "neutral")
232
+ bucket[app] = max(bucket.get(app, 0), p)
233
  return max(bucket, key=bucket.get) if bucket else "neutral"
234
 
 
 
 
 
 
 
 
 
 
 
 
235
  # ---------------- Chat logic ----------------
236
  def crisis_block(country):
237
  msg = CRISIS_NUMBERS.get(country, CRISIS_NUMBERS["Other / Not listed"])
238
+ return (f"💛 I'm really sorry you're feeling like this. You matter.\n\n"
239
+ f"{msg}\n\nPlease reach out for help you are not alone.")
 
240
 
241
  def chat_step(message, history, country, save_session):
242
  if CRISIS_RE.search(message):
 
244
  if CLOSING_RE.search(message):
245
  emotion = detect_emotions(message)
246
  tip = random.choice(SUGGESTIONS.get(emotion, SUGGESTIONS["neutral"]))
247
+ return (f"Thank you 💛 One last gentle reminder:\n\n• {tip}", "#FFFFFF")
248
+ emotion = detect_emotions(message)
 
 
249
  color = COLOR_MAP.get(emotion, "#FFFFFF")
250
  if save_session:
251
  log_session(country, message, emotion)
252
+ tip = random.choice(SUGGESTIONS.get(emotion, SUGGESTIONS["neutral"]))
253
+ reply = f"• {tip}"
254
+ if not history:
255
+ reply += "\n\n*Can you tell me a bit more about that feeling?*"
256
  return reply, color
257
 
258
+ # ---------------- Interface ----------------
259
  init_db()
260
 
261
+ with gr.Blocks(title="🪞 MoodMirror+ (Advice-only)") as demo:
262
+ style = gr.HTML("")
263
+ gr.Markdown("### 🪞 MoodMirror+ Gentle Emotional Support 🌿\n"
264
+ "Always responds with a caring self-care tip.\n\n"
265
+ "_Not medical advice. If you feel unsafe, please reach out for help immediately._")
 
 
 
 
 
 
 
 
 
266
  with gr.Row():
267
+ country = gr.Dropdown(list(CRISIS_NUMBERS.keys()), value="Other / Not listed", label="Country")
268
+ save_ok = gr.Checkbox(False, label="Save anonymized session (no personal data)")
 
 
269
  chat = gr.Chatbot(height=360)
270
+ msg = gr.Textbox(label="Your message", placeholder="Tell me how you feel...")
271
  send = gr.Button("Send")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
 
273
  def respond(user_msg, chat_hist, country_choice, save_flag):
274
+ if not user_msg.strip():
 
275
  return chat_hist + [[user_msg, "Please share a short sentence about how you feel 🙂"]], "", "", ""
276
  reply, color = chat_step(user_msg, chat_hist, country_choice, bool(save_flag))
277
  style_tag = f"<style>:root,body,.gradio-container{{background:{color}!important;}}</style>"
278
  return chat_hist + [[user_msg, reply]], "", style_tag, ""
279
 
280
+ send.click(respond, [msg, chat, country, save_ok], [chat, style, style, msg], queue=True)
281
+ msg.submit(respond, [msg, chat, country, save_ok], [chat, style, style, msg], queue=True)
 
 
282
 
283
  if __name__ == "__main__":
284
  demo.queue()