cassandrasestier commited on
Commit
f6dfb38
·
verified ·
1 Parent(s): 811d9dc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +79 -286
app.py CHANGED
@@ -1,6 +1,7 @@
1
  # ================================
2
  # 🪞 MoodMirror+ — Text Emotion • Advice-only + brief intros & reasons
3
  # - Adds an English "Emergency numbers" tab by country
 
4
  # ================================
5
  import os
6
  import re
@@ -8,6 +9,7 @@ import random
8
  import sqlite3
9
  import joblib
10
  import numpy as np
 
11
  from datetime import datetime
12
 
13
  import gradio as gr
@@ -40,212 +42,53 @@ CLOSING_RE = re.compile(
40
  re.I,
41
  )
42
 
43
- # (Original mapping kept for in-chat crisis block)
44
- CRISIS_NUMBERS = {
45
- "France": "📞 **3114** (Numéro national de prévention du suicide, 24/7)",
46
- "United States": "📞 **988** (Suicide & Crisis Lifeline, 24/7)",
47
- "Canada": "📞 **988** (Suicide Crisis Helpline, 24/7)",
48
- "United Kingdom / ROI": "📞 **116 123** (Samaritans, 24/7)",
49
- "Australia": "📞 **13 11 14** (Lifeline, 24/7)",
50
- "Other / Not listed": "Call local emergency (**112/911**) or search “suicide hotline” for your country.",
51
- }
52
-
53
- # --------- English mapping used by the new tab (expanded) ----------
54
  CRISIS_NUMBERS_EN = {
55
- # Core set you already had
56
  "United States": "📞 **988** (Suicide & Crisis Lifeline, 24/7)",
57
  "Canada": "📞 **988** (Suicide Crisis Helpline, 24/7)",
58
  "United Kingdom": "📞 **116 123** (Samaritans, 24/7)",
59
  "Ireland": "📞 **116 123** (Samaritans Ireland, 24/7)",
60
  "France": "📞 **3114** (National Suicide Prevention number, 24/7)",
61
- "Australia": "📞 **13 11 14** (Lifeline, 24/7)",
62
-
63
- # Added countries (verified public resources)
64
  "Belgium": "📞 **1813** (Zelfmoordlijn, 24/7)",
65
  "Switzerland": "📞 **143** (La Main Tendue / Heart2Heart)",
66
  "Spain": "📞 **024** (Línea 024 — Atención a la conducta suicida, 24/7)",
67
  "Germany": "📞 **0800 111 0 111** / **0800 111 0 222** / **116 123** (TelefonSeelsorge, 24/7)",
68
  "Netherlands": "📞 **0800-0113** (free) or **113** (standard rate) — 113 Suicide Prevention",
69
  "Portugal": "📞 **213 544 545**, **912 802 669**, **963 524 660** (SOS Voz Amiga)",
 
70
  "New Zealand": "📞 **0508 828 865** (Suicide Crisis Helpline — TAUTOKO)",
71
  "India": "📞 **14416** (Tele MANAS, 24/7) or **1800-599-0019** (KIRAN)",
72
  "South Africa": "📞 **0800 567 567** (SADAG Suicide Crisis Helpline, 24/7)",
73
-
74
- # Fallback
75
  "Other / Not listed": "Call local emergency (**112/911**) or search “suicide hotline” + your country.",
76
  }
77
 
78
- # ---------------- Advice library (concise, actionable) ----------------
79
  SUGGESTIONS = {
80
- "sadness": [
81
- "Go for a 5-minute outside walk and name three colors you see.",
82
- "Write what hurts, then add one thing you still care about.",
83
- "Take a warm shower and focus on the feeling on your shoulders.",
84
- "Message a safe person: “Can I vent for 2 minutes?”",
85
- "Eat something simple and drink a big glass of water.",
86
- "Play one song that matches your mood don’t hide it.",
87
- "Wrap yourself in a blanket and slow your exhale for 60 seconds.",
88
- "List three small things that kept you going today.",
89
- "Tidy one tiny area (corner of the desk or sink).",
90
- "Watch something gentle or nostalgic for 10 minutes.",
91
- "Place a hand on your chest and repeat: “This will pass.”",
92
- "Write a note to your future self: “You made it through today.”",
93
- ],
94
- "fear": [
95
- "Do 5-4-3-2-1 grounding: 5 see, 4 feel, 3 hear, 2 smell, 1 taste.",
96
- "Make your exhale longer than your inhale for eight breaths.",
97
- "Turn on a light or soft music to signal safety to your body.",
98
- "Name the fear out loud in one sentence.",
99
- "Hold something cold in your hand for 30 seconds.",
100
- "Tell yourself: “Just the next minute — that’s all.”",
101
- "Stand up and shake out your hands and arms.",
102
- "Write the worst case, then the most likely case beside it.",
103
- "Walk while counting your steps slowly to 100.",
104
- "Mute news/scrolling for the next 30 minutes.",
105
- "Sit with back supported and feet flat; feel the contact points.",
106
- "Repeat softly: “This feeling is temporary.”",
107
- ],
108
- "anger": [
109
- "Take space before replying; set a 10-minute timer.",
110
- "Do ten slow exhales through pursed lips.",
111
- "Write an uncensored note and delete it afterward.",
112
- "Splash cool water on your face or wrists.",
113
- "Walk fast for five minutes or climb one set of stairs.",
114
- "Name the crossed boundary and craft one calm sentence.",
115
- "Squeeze then release your fists ten times.",
116
- "Clean a small area to discharge energy.",
117
- "Postpone the decision until you feel steady again.",
118
- "Say: “I’m not ready to talk yet; I’ll come back later.”",
119
- "Ask yourself: “What hurt sits under this anger?”",
120
- "Drop your shoulders and unclench your jaw.",
121
- ],
122
- "nervousness": [
123
- "4-7-8 breathing: in 4s, hold 7s, out 8s (four rounds).",
124
- "Relax your jaw and lower your shoulders.",
125
- "Write worries down; cross out what you can’t control.",
126
- "Pick one tiny action you can finish in five minutes.",
127
- "Trace a square with your finger; breathe in on each side.",
128
- "Walk while matching steps to slower breaths.",
129
- "Sit by daylight or open a window for fresh air.",
130
- "Say: “I can do this one step at a time.”",
131
- "Pause caffeine for the next few hours.",
132
- "Give yourself a 2-minute pause — eyes on one calm point.",
133
- "Hold a warm mug and notice the heat.",
134
- "Stretch your arms overhead and widen your posture.",
135
- ],
136
- "boredom": [
137
- "Set a 2-minute timer and start anything small.",
138
- "Change your soundtrack — put on one new song.",
139
- "Shift a few objects on your desk for a new view.",
140
- "Read one paragraph on a random topic.",
141
- "Doodle or hum for 90 seconds without judging it.",
142
- "Step outside; look up and find three shapes in the sky.",
143
- "Write five ideas quickly without editing.",
144
- "Do 15 jumping jacks or a quick stretch.",
145
- "Clean your phone screen or keyboard.",
146
- "Try a different drink or snack.",
147
- "Learn one keyboard shortcut you’ll use today.",
148
- "Send a simple “how are you?” to someone.",
149
- ],
150
- "grief": [
151
- "Hold a photo or object and say their name softly.",
152
- "Drink water and eat something — your body grieves too.",
153
- "Write a short letter to them about today.",
154
- "Rest without guilt — sorrow is heavy work.",
155
- "Share one memory you want to keep vivid.",
156
- "Let tears come when they need to.",
157
- "Light a candle and sit quietly for two minutes.",
158
- "Walk somewhere meaningful and notice what rises.",
159
- "Create a small ritual to honor them (song, place, phrase).",
160
- "Plan one kind thing for yourself this week.",
161
- "Say: “Missing you means I loved you.”",
162
- "Plant your feet and breathe into your belly.",
163
- ],
164
- "love": [
165
- "Send a kind message without expecting a reply.",
166
- "Note three things you appreciate about someone close.",
167
- "Offer yourself one gentle act you needed today.",
168
- "Listen fully to someone for one uninterrupted minute.",
169
- "Give a sincere compliment to a stranger.",
170
- "Prepare something with care for someone you value.",
171
- "Say “thank you” out loud for one small thing.",
172
- "Ask a caring question and wait for the answer.",
173
- "Write what love means to you in three lines.",
174
- "Plan a tiny gesture for tomorrow.",
175
- "Look at your face kindly in the mirror for 10 seconds.",
176
- "Let yourself accept help today.",
177
- ],
178
- "joy": [
179
- "Pause and take three slow breaths to savor this.",
180
- "Capture it — photo, note, or voice memo.",
181
- "Tell someone why you feel good right now.",
182
- "Move to music for one song.",
183
- "Notice where the joy sits in your body.",
184
- "Prepare a small treat to celebrate.",
185
- "Write one line starting with “I’m glad that…”.",
186
- "Do one kind act while you feel resourced.",
187
- "Enjoy one minute of quiet appreciation.",
188
- "Plan a tiny celebration later today.",
189
- "Share a smile with someone nearby.",
190
- "Thank yourself for the steps that led here.",
191
- ],
192
- "curiosity": [
193
- "Search one concept and read just the first paragraph.",
194
- "Ask a question you’ve never asked a friend.",
195
- "Watch a “how does X work?” video for 3 minutes.",
196
- "Write three quick “what if…?” ideas.",
197
- "Take apart a small idea or object (safely) and observe.",
198
- "Teach someone one thing you learned today.",
199
- "Open a random article and summarize it in one line.",
200
- "Try a new route or viewpoint in your space.",
201
- "Learn one new word and use it once.",
202
- "List five topics you’d like to explore.",
203
- "Read one thread in a community you care about.",
204
- "Sketch a simple diagram of an idea.",
205
- ],
206
- "gratitude": [
207
- "List three tiny things that made today easier.",
208
- "Thank someone by name for something specific.",
209
- "Notice an everyday object and appreciate its help.",
210
- "Take a photo of a small comfort.",
211
- "Write “I’m lucky that…” and finish it once.",
212
- "Send a short “thinking of you”.",
213
- "Savor your next sip or bite with attention.",
214
- "Name one privilege you have today.",
215
- "Say thank you silently to your body.",
216
- "Share one good thing with a friend.",
217
- "Keep a short gratitude note in your phone.",
218
- "Place a small reminder where you’ll see it tomorrow.",
219
- ],
220
- "neutral": [
221
- "Take one slow breath and relax your hands.",
222
- "Notice a color, a texture, and a sound around you.",
223
- "Plan one tiny task to finish today.",
224
- "Drink a glass of water mindfully.",
225
- "Stand, stretch, and roll your shoulders.",
226
- "Step outside for two minutes of fresh air.",
227
- "Wipe your screen or desk for a reset.",
228
- "Organize three items in your space.",
229
- "Set a 10-minute timer to focus on one thing.",
230
- "Do a gentle 30-second neck stretch.",
231
- "Check your posture; support your back.",
232
- "Open a window and take three breaths.",
233
- ],
234
  }
235
 
236
- # One-line reasons
237
  WHY_BY_EMOTION = {
238
- "sadness": "Small sensory and connection cues can ease low mood and restore momentum.",
239
- "fear": "Grounding + longer exhales calm the threat system and signal safety.",
240
- "anger": "Space + movement lower adrenaline so you can respond, not react.",
241
- "nervousness": "Slow breathing and micro-actions reduce anxious energy and create control.",
242
- "boredom": "Novelty and tiny starts re-engage attention and kick-off motivation.",
243
- "grief": "Rituals and gentle care help your body carry love and loss together.",
244
- "love": "Expressing and receiving care strengthens bonds and self-kindness.",
245
- "joy": "Savoring and sharing consolidate positive memories and resilience.",
246
- "curiosity": "Small explorations feed learning circuits and open perspective.",
247
- "gratitude": "Noticing support shifts attention toward resources and strengths.",
248
- "neutral": "Simple body care keeps your baseline steady for the rest of the day.",
249
  }
250
 
251
  COLOR_MAP = {
@@ -256,19 +99,11 @@ COLOR_MAP = {
256
  "neutral": "#F5F5F5", "curiosity": "#E6EE9C",
257
  }
258
 
259
- # Map 28 GoEmotions -> UI buckets
260
  GOEMO_TO_APP = {
261
- "admiration": "gratitude", "amusement": "joy", "anger": "anger", "annoyance": "anger",
262
- "approval": "gratitude", "caring": "love", "confusion": "nervousness",
263
- "curiosity": "curiosity", "desire": "joy", "disappointment": "sadness",
264
- "disapproval": "anger", "disgust": "anger", "embarrassment": "nervousness",
265
- "excitement": "joy", "fear": "fear", "gratitude": "gratitude", "grief": "grief",
266
- "joy": "joy", "love": "love", "nervousness": "nervousness", "optimism": "joy",
267
- "pride": "joy", "realization": "neutral", "relief": "gratitude", "remorse": "grief",
268
- "sadness": "sadness", "surprise": "neutral", "neutral": "neutral",
269
  }
270
 
271
- # ---------------- Preprocessing & thresholds (text) ----------------
272
  THRESHOLD_BASE = 0.30
273
  MIN_THRESHOLD = 0.10
274
  CLEAN_RE = re.compile(r"(https?://\S+)|(@\w+)|(#\w+)|[^a-zA-Z0-9\s']")
@@ -279,37 +114,8 @@ def clean_text(s: str) -> str:
279
  s = re.sub(r"\s+", " ", s).strip()
280
  return s
281
 
282
- EMOJI_HINTS = {"😭": "sadness", "😡": "anger", "🥰": "love", "😨": "fear", "😴": "boredom"}
283
- NEGATION_HINTS_EN = {
284
- "not happy": "sadness", "not ok": "sadness", "no energy": "boredom",
285
- "can't focus": "nervousness", "cannot focus": "nervousness"
286
- }
287
- HINTS_FR = {
288
- "pas bien": "sadness", "triste": "sadness", "j'ai peur": "fear",
289
- "angoisse": "nervousness", "anxieux": "nervousness",
290
- "fatigué": "sadness", "épuisé": "sadness",
291
- }
292
-
293
  def augment_text(text: str, history=None) -> str:
294
- t = clean_text(text or "")
295
- hints = []
296
- for k in EMOJI_HINTS:
297
- if k in (text or ""):
298
- hints.append(EMOJI_HINTS[k])
299
- for k in NEGATION_HINTS_EN:
300
- if k in t:
301
- hints.append(NEGATION_HINTS_EN[k])
302
- lt = (text or "").lower()
303
- for k in HINTS_FR:
304
- if k in lt:
305
- hints.append(HINTS_FR[k])
306
- if history and len(t.split()) < 8:
307
- prev_user = history[-1][0] if history and history[-1] else ""
308
- if isinstance(prev_user, str) and prev_user:
309
- t = t + " " + clean_text(prev_user)
310
- if hints:
311
- t = t + " " + " ".join([f"emo_{h}" for h in hints])
312
- return t
313
 
314
  # ---------------- SQLite ----------------
315
  def get_conn():
@@ -331,7 +137,7 @@ def log_session(country, msg, emotion):
331
  conn.commit()
332
  conn.close()
333
 
334
- # ---------------- Text model: GoEmotions dataset-only ----------------
335
  def load_goemotions_dataset():
336
  ds = load_dataset("google-research-datasets/go_emotions", "simplified")
337
  return ds, ds["train"].features["labels"].feature.names
@@ -347,10 +153,7 @@ def train_or_load_model():
347
  Y_train = mlb.fit_transform(y_train)
348
  clf = Pipeline([
349
  ("tfidf", TfidfVectorizer(lowercase=True, ngram_range=(1,2), min_df=2, max_df=0.9, strip_accents="unicode")),
350
- ("ovr", OneVsRestClassifier(
351
- LogisticRegression(solver="saga", max_iter=1000, class_weight="balanced"),
352
- n_jobs=-1
353
- ))
354
  ])
355
  clf.fit(X_train, Y_train)
356
  joblib.dump({"version": MODEL_VERSION, "pipeline": clf, "mlb": mlb, "label_names": names}, MODEL_PATH)
@@ -362,7 +165,6 @@ except Exception as e:
362
  print("[ERROR] Model load/train:", e)
363
  CLASSIFIER, MLB, LABEL_NAMES = None, None, None
364
 
365
- # ---------------- Inference: TEXT ----------------
366
  def classify_text(text_augmented: str):
367
  if not CLASSIFIER: return []
368
  proba = CLASSIFIER.predict_proba([text_augmented])[0]
@@ -382,7 +184,7 @@ def detect_emotion_text(message: str, history):
382
  bucket[app] = max(bucket.get(app, 0.0), p)
383
  return max(bucket, key=bucket.get) if bucket else "neutral"
384
 
385
- # ---------------- Advice selection ----------------
386
  def pick_advice_from_pool(emotion: str, pool: dict, last_tip: str = ""):
387
  tips_all = SUGGESTIONS.get(emotion, SUGGESTIONS["neutral"])
388
  entry = pool.get(emotion, {"unused": [], "last": ""})
@@ -399,18 +201,13 @@ def format_reply(emotion: str, tip: str) -> str:
399
  why = WHY_BY_EMOTION.get(emotion, WHY_BY_EMOTION["neutral"])
400
  return f"Try this now:\n• {tip}\n_(Why it helps: {why})_"
401
 
402
- # ---------------- Replies ----------------
403
- def crisis_block(country):
404
- msg = CRISIS_NUMBERS.get(country, CRISIS_NUMBERS["Other / Not listed"])
405
- return f"💛 You matter. If you're in danger or thinking of harming yourself, please reach out now.\n\n{msg}"
406
-
407
  def crisis_block_en(country):
408
  msg = CRISIS_NUMBERS_EN.get(country, CRISIS_NUMBERS_EN["Other / Not listed"])
409
  return "💛 You matter. If you're in danger or thinking of harming yourself, please reach out now.\n\n" + msg
410
 
411
  def chat_step(user_text, history, country, save_session, advice_pool):
412
  if user_text and CRISIS_RE.search(user_text):
413
- return crisis_block(country), "#FFD6E7", "neutral", "", advice_pool
414
  if user_text and CLOSING_RE.search(user_text):
415
  emotion = "neutral"
416
  tip, advice_pool = pick_advice_from_pool(emotion, advice_pool)
@@ -429,26 +226,19 @@ init_db()
429
 
430
  with gr.Blocks(title="🪞 MoodMirror+ — Text Emotion • Advice-only") as demo:
431
  style = gr.HTML("")
432
- gr.Markdown(
433
- "### 🪞 MoodMirror+ — Emotion-aware advice (text-only)\n"
434
- "Classifier trained on GoEmotions (dataset-only). Each message is analysed anew.\n\n"
435
- "_Not medical advice. If unsafe, please reach out for help._"
436
- )
437
 
438
  with gr.Tabs():
439
- # ---- Tab 1: Advice (chat) ----
440
  with gr.Tab("Advice"):
441
  with gr.Row():
442
- country = gr.Dropdown(list(CRISIS_NUMBERS.keys()), value="Other / Not listed", label="Country")
443
  save_ok = gr.Checkbox(False, label="Save anonymized session")
444
-
445
  chat = gr.Chatbot(height=380)
446
  msg = gr.Textbox(label="Your message", placeholder="Share how you feel...")
447
-
448
  with gr.Row():
449
  send = gr.Button("Send", variant="primary")
450
  regen = gr.Button("🔁 New advice", variant="secondary")
451
-
452
  last_emotion = gr.State("neutral")
453
  last_tip = gr.State("")
454
  advice_pool = gr.State({})
@@ -465,49 +255,52 @@ with gr.Blocks(title="🪞 MoodMirror+ — Text Emotion • Advice-only") as dem
465
  reply = format_reply(_emotion, tip)
466
  return chat_hist + [[None, reply]], "", _emotion, tip, _pool
467
 
468
- send.click(
469
- respond,
470
- inputs=[msg, chat, country, save_ok, last_emotion, last_tip, advice_pool],
471
- outputs=[chat, style, last_emotion, last_tip, advice_pool],
472
- queue=True
473
- )
474
- msg.submit(
475
- respond,
476
- inputs=[msg, chat, country, save_ok, last_emotion, last_tip, advice_pool],
477
- outputs=[chat, style, last_emotion, last_tip, advice_pool],
478
- queue=True
479
- )
480
- regen.click(
481
- new_advice,
482
- inputs=[chat, last_emotion, last_tip, advice_pool],
483
- outputs=[chat, style, last_emotion, last_tip, advice_pool],
484
- queue=True
485
- )
486
-
487
- # ---- Tab 2: Emergency numbers (EN) ----
488
  with gr.Tab("Emergency numbers"):
489
- gr.Markdown(
490
- "#### 📟 Emergency numbers by country\n"
491
- "Select your country to see the recommended crisis line. "
492
- "If you’re in immediate danger, call local emergency services (**112/911**) right away."
493
- )
494
-
495
- country_view = gr.Dropdown(
496
- choices=list(CRISIS_NUMBERS_EN.keys()),
497
- value="United States",
498
- label="Country"
499
- )
500
  crisis_info = gr.Markdown(value=crisis_block_en("United States"))
501
-
502
- def show_crisis_for_country_en(c):
503
- return crisis_block_en(c)
504
-
505
  country_view.change(show_crisis_for_country_en, inputs=country_view, outputs=crisis_info)
 
506
 
507
- gr.Markdown(
508
- "> ℹ️ Numbers may change. If a line doesn’t work or your country isn’t listed, "
509
- "call local emergency services (**112/911**) or search **“suicide hotline” + your country**."
510
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511
 
512
  if __name__ == "__main__":
513
  demo.queue()
 
1
  # ================================
2
  # 🪞 MoodMirror+ — Text Emotion • Advice-only + brief intros & reasons
3
  # - Adds an English "Emergency numbers" tab by country
4
+ # - Adds a "Breathing" guided exercise tab
5
  # ================================
6
  import os
7
  import re
 
9
  import sqlite3
10
  import joblib
11
  import numpy as np
12
+ import time
13
  from datetime import datetime
14
 
15
  import gradio as gr
 
42
  re.I,
43
  )
44
 
45
+ # English crisis numbers (expanded)
 
 
 
 
 
 
 
 
 
 
46
  CRISIS_NUMBERS_EN = {
 
47
  "United States": "📞 **988** (Suicide & Crisis Lifeline, 24/7)",
48
  "Canada": "📞 **988** (Suicide Crisis Helpline, 24/7)",
49
  "United Kingdom": "📞 **116 123** (Samaritans, 24/7)",
50
  "Ireland": "📞 **116 123** (Samaritans Ireland, 24/7)",
51
  "France": "📞 **3114** (National Suicide Prevention number, 24/7)",
 
 
 
52
  "Belgium": "📞 **1813** (Zelfmoordlijn, 24/7)",
53
  "Switzerland": "📞 **143** (La Main Tendue / Heart2Heart)",
54
  "Spain": "📞 **024** (Línea 024 — Atención a la conducta suicida, 24/7)",
55
  "Germany": "📞 **0800 111 0 111** / **0800 111 0 222** / **116 123** (TelefonSeelsorge, 24/7)",
56
  "Netherlands": "📞 **0800-0113** (free) or **113** (standard rate) — 113 Suicide Prevention",
57
  "Portugal": "📞 **213 544 545**, **912 802 669**, **963 524 660** (SOS Voz Amiga)",
58
+ "Australia": "📞 **13 11 14** (Lifeline, 24/7)",
59
  "New Zealand": "📞 **0508 828 865** (Suicide Crisis Helpline — TAUTOKO)",
60
  "India": "📞 **14416** (Tele MANAS, 24/7) or **1800-599-0019** (KIRAN)",
61
  "South Africa": "📞 **0800 567 567** (SADAG Suicide Crisis Helpline, 24/7)",
 
 
62
  "Other / Not listed": "Call local emergency (**112/911**) or search “suicide hotline” + your country.",
63
  }
64
 
65
+ # ---------------- Advice library (concise) ----------------
66
  SUGGESTIONS = {
67
+ "sadness": ["Go for a 5-minute outside walk and name three colors you see."],
68
+ "fear": ["Do 5-4-3-2-1 grounding: 5 see, 4 feel, 3 hear, 2 smell, 1 taste."],
69
+ "anger": ["Take space before replying; set a 10-minute timer."],
70
+ "nervousness": ["4-7-8 breathing: in 4s, hold 7s, out 8s (four rounds)."],
71
+ "boredom": ["Set a 2-minute timer and start anything small."],
72
+ "grief": ["Hold a photo or object and say their name softly."],
73
+ "love": ["Send a kind message without expecting a reply."],
74
+ "joy": ["Pause and take three slow breaths to savor this."],
75
+ "curiosity": ["Search one concept and read just the first paragraph."],
76
+ "gratitude": ["List three tiny things that made today easier."],
77
+ "neutral": ["Take one slow breath and relax your hands."],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  }
79
 
 
80
  WHY_BY_EMOTION = {
81
+ "sadness": "Small sensory and connection cues can ease low mood.",
82
+ "fear": "Grounding + longer exhales calm the threat system.",
83
+ "anger": "Space + movement lower adrenaline to respond, not react.",
84
+ "nervousness": "Slow breathing and micro-actions reduce anxious energy.",
85
+ "boredom": "Novelty and small starts re-engage attention.",
86
+ "grief": "Rituals and gentle care help carry love and loss.",
87
+ "love": "Expressing care strengthens bonds and self-kindness.",
88
+ "joy": "Savoring and sharing consolidate positive memories.",
89
+ "curiosity": "Small explorations feed learning and perspective.",
90
+ "gratitude": "Noticing support shifts attention toward strengths.",
91
+ "neutral": "Simple body care keeps your baseline steady.",
92
  }
93
 
94
  COLOR_MAP = {
 
99
  "neutral": "#F5F5F5", "curiosity": "#E6EE9C",
100
  }
101
 
 
102
  GOEMO_TO_APP = {
103
+ "sadness": "sadness", "joy": "joy", "fear": "fear", "anger": "anger", "neutral": "neutral",
 
 
 
 
 
 
 
104
  }
105
 
106
+ # ---------------- Preprocessing ----------------
107
  THRESHOLD_BASE = 0.30
108
  MIN_THRESHOLD = 0.10
109
  CLEAN_RE = re.compile(r"(https?://\S+)|(@\w+)|(#\w+)|[^a-zA-Z0-9\s']")
 
114
  s = re.sub(r"\s+", " ", s).strip()
115
  return s
116
 
 
 
 
 
 
 
 
 
 
 
 
117
  def augment_text(text: str, history=None) -> str:
118
+ return clean_text(text or "")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
  # ---------------- SQLite ----------------
121
  def get_conn():
 
137
  conn.commit()
138
  conn.close()
139
 
140
+ # ---------------- Model ----------------
141
  def load_goemotions_dataset():
142
  ds = load_dataset("google-research-datasets/go_emotions", "simplified")
143
  return ds, ds["train"].features["labels"].feature.names
 
153
  Y_train = mlb.fit_transform(y_train)
154
  clf = Pipeline([
155
  ("tfidf", TfidfVectorizer(lowercase=True, ngram_range=(1,2), min_df=2, max_df=0.9, strip_accents="unicode")),
156
+ ("ovr", OneVsRestClassifier(LogisticRegression(solver="saga", max_iter=1000, class_weight="balanced"), n_jobs=-1))
 
 
 
157
  ])
158
  clf.fit(X_train, Y_train)
159
  joblib.dump({"version": MODEL_VERSION, "pipeline": clf, "mlb": mlb, "label_names": names}, MODEL_PATH)
 
165
  print("[ERROR] Model load/train:", e)
166
  CLASSIFIER, MLB, LABEL_NAMES = None, None, None
167
 
 
168
  def classify_text(text_augmented: str):
169
  if not CLASSIFIER: return []
170
  proba = CLASSIFIER.predict_proba([text_augmented])[0]
 
184
  bucket[app] = max(bucket.get(app, 0.0), p)
185
  return max(bucket, key=bucket.get) if bucket else "neutral"
186
 
187
+ # ---------------- Advice logic ----------------
188
  def pick_advice_from_pool(emotion: str, pool: dict, last_tip: str = ""):
189
  tips_all = SUGGESTIONS.get(emotion, SUGGESTIONS["neutral"])
190
  entry = pool.get(emotion, {"unused": [], "last": ""})
 
201
  why = WHY_BY_EMOTION.get(emotion, WHY_BY_EMOTION["neutral"])
202
  return f"Try this now:\n• {tip}\n_(Why it helps: {why})_"
203
 
 
 
 
 
 
204
  def crisis_block_en(country):
205
  msg = CRISIS_NUMBERS_EN.get(country, CRISIS_NUMBERS_EN["Other / Not listed"])
206
  return "💛 You matter. If you're in danger or thinking of harming yourself, please reach out now.\n\n" + msg
207
 
208
  def chat_step(user_text, history, country, save_session, advice_pool):
209
  if user_text and CRISIS_RE.search(user_text):
210
+ return crisis_block_en(country), "#FFD6E7", "neutral", "", advice_pool
211
  if user_text and CLOSING_RE.search(user_text):
212
  emotion = "neutral"
213
  tip, advice_pool = pick_advice_from_pool(emotion, advice_pool)
 
226
 
227
  with gr.Blocks(title="🪞 MoodMirror+ — Text Emotion • Advice-only") as demo:
228
  style = gr.HTML("")
229
+ gr.Markdown("### 🪞 MoodMirror+ — Emotion-aware advice (text-only)\n_Not medical advice. If unsafe, please reach out for help._")
 
 
 
 
230
 
231
  with gr.Tabs():
232
+ # ---- Tab 1: Advice ----
233
  with gr.Tab("Advice"):
234
  with gr.Row():
235
+ country = gr.Dropdown(list(CRISIS_NUMBERS_EN.keys()), value="United States", label="Country")
236
  save_ok = gr.Checkbox(False, label="Save anonymized session")
 
237
  chat = gr.Chatbot(height=380)
238
  msg = gr.Textbox(label="Your message", placeholder="Share how you feel...")
 
239
  with gr.Row():
240
  send = gr.Button("Send", variant="primary")
241
  regen = gr.Button("🔁 New advice", variant="secondary")
 
242
  last_emotion = gr.State("neutral")
243
  last_tip = gr.State("")
244
  advice_pool = gr.State({})
 
255
  reply = format_reply(_emotion, tip)
256
  return chat_hist + [[None, reply]], "", _emotion, tip, _pool
257
 
258
+ send.click(respond, inputs=[msg, chat, country, save_ok, last_emotion, last_tip, advice_pool],
259
+ outputs=[chat, style, last_emotion, last_tip, advice_pool])
260
+ msg.submit(respond, inputs=[msg, chat, country, save_ok, last_emotion, last_tip, advice_pool],
261
+ outputs=[chat, style, last_emotion, last_tip, advice_pool])
262
+ regen.click(new_advice, inputs=[chat, last_emotion, last_tip, advice_pool],
263
+ outputs=[chat, style, last_emotion, last_tip, advice_pool])
264
+
265
+ # ---- Tab 2: Emergency numbers ----
 
 
 
 
 
 
 
 
 
 
 
 
266
  with gr.Tab("Emergency numbers"):
267
+ gr.Markdown("#### 📟 Emergency numbers by country\nSelect your country to see the recommended crisis line.")
268
+ country_view = gr.Dropdown(choices=list(CRISIS_NUMBERS_EN.keys()), value="United States", label="Country")
 
 
 
 
 
 
 
 
 
269
  crisis_info = gr.Markdown(value=crisis_block_en("United States"))
270
+ def show_crisis_for_country_en(c): return crisis_block_en(c)
 
 
 
271
  country_view.change(show_crisis_for_country_en, inputs=country_view, outputs=crisis_info)
272
+ gr.Markdown("> ℹ️ If your country is missing, call **112/911** or search “suicide hotline” + your country.")
273
 
274
+ # ---- Tab 3: Breathing ----
275
+ with gr.Tab("Breathing"):
276
+ gr.Markdown("#### 🌬️ Guided breathing\nPick a pattern and cycles, then press **Start**. "
277
+ "If you feel dizzy, stop and breathe normally.")
278
+ with gr.Row():
279
+ pattern = gr.Dropdown(
280
+ choices=["4-7-8", "Box (4-4-4-4)", "Coherent (5-5, ~6 breaths/min)"],
281
+ value="4-7-8", label="Pattern")
282
+ cycles = gr.Slider(1, 10, value=4, step=1, label="Number of cycles")
283
+ start_btn = gr.Button("Start", variant="primary")
284
+ breathe_out = gr.Markdown()
285
+
286
+ def _steps_for(p):
287
+ if p == "4-7-8": return [("Inhale", 4), ("Hold", 7), ("Exhale", 8)]
288
+ elif p.startswith("Box"): return [("Inhale", 4), ("Hold", 4), ("Exhale", 4), ("Hold", 4)]
289
+ else: return [("Inhale", 5), ("Exhale", 5)]
290
+
291
+ def run_breathing(pat, n):
292
+ steps = _steps_for(pat)
293
+ yield "Starting in 3…"; time.sleep(1)
294
+ yield "Starting in 2…"; time.sleep(1)
295
+ yield "Starting in 1…"; time.sleep(1)
296
+ for c in range(1, int(n) + 1):
297
+ for label, secs in steps:
298
+ for t in range(secs, 0, -1):
299
+ dots = "•" * (secs - t + 1)
300
+ yield f"**Cycle {c}/{int(n)}** \n**{label}** — {t}s \n{dots}"
301
+ time.sleep(1)
302
+ yield "✅ Done. Notice how your body feels."
303
+ start_btn.click(run_breathing, inputs=[pattern, cycles], outputs=[breathe_out])
304
 
305
  if __name__ == "__main__":
306
  demo.queue()