cassandrasestier commited on
Commit
c36e1c8
·
verified ·
1 Parent(s): 1500034

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +179 -223
app.py CHANGED
@@ -1,13 +1,14 @@
1
  # ================================
2
- # 🪞 MoodMirror+ — Conversational Emotional Self-Care
3
- # Advice-only edition + "🔁 New advice" button
4
- # Dataset-only (GoEmotions): TF-IDF + OneVsRest Logistic Regression
5
  # ================================
6
  import os
7
  import re
8
  import random
9
  import sqlite3
10
  import joblib
 
11
  from datetime import datetime
12
 
13
  import gradio as gr
@@ -17,7 +18,6 @@ from sklearn.preprocessing import MultiLabelBinarizer
17
  from sklearn.linear_model import LogisticRegression
18
  from sklearn.multiclass import OneVsRestClassifier
19
  from sklearn.pipeline import Pipeline
20
- from sklearn.metrics import f1_score
21
 
22
  # ---------------- Storage paths ----------------
23
  def _pick_data_dir():
@@ -25,13 +25,13 @@ def _pick_data_dir():
25
  return "/data"
26
  return os.getcwd()
27
 
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 = "v4-advice-only+regen"
33
 
34
- # ---------------- Crisis detection ----------------
35
  CRISIS_RE = re.compile(
36
  r"\b(self[- ]?harm|suicid|kill myself|end my life|overdose|cutting|i don.?t want to live|can.?t go on)\b",
37
  re.I,
@@ -42,83 +42,41 @@ CLOSING_RE = re.compile(
42
  )
43
 
44
  CRISIS_NUMBERS = {
45
- "United States": "Call or text **988** (Suicide & Crisis Lifeline, 24/7).",
46
- "France": "Call **3114** (Numéro national de prévention du suicide, 24/7).",
47
- "United Kingdom / ROI": "Call **116 123** (Samaritans, 24/7).",
48
- "Canada": "Call or text **988** (Suicide Crisis Helpline).",
49
- "Australia": "Call **13 11 14** (Lifeline).",
50
- "Other / Not listed": "Call your local emergency number (**112/911**) or search 'suicide hotline' in your country.",
51
  }
52
 
53
  # ---------------- Advice library ----------------
54
  SUGGESTIONS = {
55
- "sadness": [
56
- "Be gentle with yourself. Rest, cry, or connect it’s okay to feel low.",
57
- "Let yourself breathe; you don’t have to fix everything today.",
58
- "Try writing down what hurts and what you wish someone could say to you.",
59
- "Drink water, open a window, or take a short walk — small acts help.",
60
- "Remember, sadness passes more easily when you let it exist.",
61
- ],
62
- "fear": [
63
- "Ground yourself name 5 things you see, 4 you feel, 3 you hear.",
64
- "You are safe in this moment; focus on your breath.",
65
- "Not every thought is a fact notice which ones just want attention.",
66
- "Take slow breaths; safety starts with one calm inhale.",
67
- ],
68
- "joy": [
69
- "Let yourself smile and enjoy it fully.",
70
- "Pause and notice how joy feels in your body.",
71
- "Share a kind word or message happiness grows when shared.",
72
- "Write down one thing that made you smile today.",
73
- ],
74
- "anger": [
75
- "Pause before reacting; give yourself time to cool down.",
76
- "Take deep breaths in through the nose, out slowly through the mouth.",
77
- "Try walking or stretching to release the tension.",
78
- "Ask yourself what boundary was crossed and how you can protect it calmly.",
79
- ],
80
- "boredom": [
81
- "Try something small and new — even a 2-minute change matters.",
82
- "Move a little: tidy your space or step outside for fresh air.",
83
- "Write down one creative idea, no matter how silly it feels.",
84
- "Sometimes rest looks like boredom — let it recharge you.",
85
- ],
86
- "grief": [
87
- "Let the memories come — it’s okay to cry or miss someone deeply.",
88
- "Hold an object that reminds you of love, not loss.",
89
- "Eat, drink water, and rest — your body also grieves.",
90
- "You don’t have to move on; you can move forward while remembering.",
91
- ],
92
- "love": [
93
- "Reach out to someone you care about — a few words can mean a lot.",
94
- "Take a deep breath and remind yourself that you are loved too.",
95
- "Do one small act of kindness for yourself or another.",
96
- "Love doesn’t have to be loud; quiet care counts too.",
97
- ],
98
- "nervousness": [
99
- "Relax your shoulders, unclench your jaw, and breathe slowly.",
100
- "Write your worries down, then cross out what you can’t control.",
101
- "Try the 4-7-8 breath: inhale 4, hold 7, exhale 8.",
102
- "Tell yourself: 'I can handle this one moment at a time.'",
103
- ],
104
- "curiosity": [
105
- "Follow what interests you, even if it seems random.",
106
- "Ask one new question today — curiosity keeps your mind alive.",
107
- "Try learning something small with no pressure to master it.",
108
- "Explore a thought just because it feels interesting.",
109
- ],
110
- "gratitude": [
111
- "Name three things you’re grateful for right now.",
112
- "Say thank you — even silently — for something that helped you.",
113
- "Take a photo or note of something simple that brings comfort.",
114
- "Remember that small joys count just as much as big ones.",
115
- ],
116
- "neutral": [
117
- "Take a slow, conscious breath and relax your body.",
118
- "Notice one pleasant detail around you — sound, color, or scent.",
119
- "Sit quietly for a minute; calm moments build strength.",
120
- "Stretch or move — it helps your mood reset naturally.",
121
- ],
122
  }
123
 
124
  COLOR_MAP = {
@@ -126,71 +84,52 @@ COLOR_MAP = {
126
  "sadness": "#BBDEFB", "grief": "#B3E5FC",
127
  "fear": "#E1BEE7", "nervousness": "#E1BEE7",
128
  "anger": "#FFCCBC", "boredom": "#E0E0E0",
129
- "neutral": "#F5F5F5",
130
  }
131
 
132
- GOEMO_TO_APP = {
133
- "admiration": "gratitude", "amusement": "joy", "anger": "anger", "annoyance": "anger",
134
- "approval": "gratitude", "caring": "love", "confusion": "nervousness",
135
- "curiosity": "curiosity", "desire": "joy", "disappointment": "sadness",
136
- "disapproval": "anger", "disgust": "anger", "embarrassment": "nervousness",
137
- "excitement": "joy", "fear": "fear", "gratitude": "gratitude", "grief": "grief",
138
- "joy": "joy", "love": "love", "nervousness": "nervousness", "optimism": "joy",
139
- "pride": "joy", "realization": "neutral", "relief": "gratitude", "remorse": "grief",
140
- "sadness": "sadness", "surprise": "neutral", "neutral": "neutral",
141
- }
142
 
143
- # ---------------- Model configuration ----------------
144
- THRESHOLD = 0.3
145
- MIN_THRESHOLD = 0.12
146
- TOP1_FALLBACK = True
147
-
148
- # ---------------- Helper: augment text ----------------
149
- EMOJI_HINTS = {"😢": "sadness", "😭": "sadness", "😡": "anger", "😍": "love", "🤔": "curiosity"}
150
- SLANG_HINTS = {"idk": "confusion", "meh": "boredom", "ugh": "annoyance", "im fine": "sadness", "i'm fine": "sadness"}
151
- NEGATION_PATTERNS = [("not happy", "sadness"), ("not ok", "sadness"), ("no motivation", "boredom")]
152
-
153
- def augment_text(text):
154
- t = text.lower()
155
- hints = []
156
- for ch in text:
157
- if ch in EMOJI_HINTS: hints.append(EMOJI_HINTS[ch])
158
- for k, v in SLANG_HINTS.items():
159
- if k in t: hints.append(v)
160
- for pat, lab in NEGATION_PATTERNS:
161
- if pat in t: hints.append(lab)
162
- if hints:
163
- return text + " " + " ".join([f"emo_{h}" for h in hints])
164
- return text
165
-
166
- # ---------------- DB setup ----------------
167
  def get_conn():
168
  return sqlite3.connect(DB_PATH, check_same_thread=False, timeout=10)
169
 
170
  def init_db():
171
  conn = get_conn()
172
- c = conn.cursor()
173
- c.execute("""CREATE TABLE IF NOT EXISTS sessions(
174
  id INTEGER PRIMARY KEY AUTOINCREMENT,
175
- ts TEXT, country TEXT, user_text TEXT, main_emotion TEXT)""")
 
176
  conn.commit()
177
  conn.close()
178
 
179
  def log_session(country, msg, emotion):
180
  conn = get_conn()
181
- c = conn.cursor()
182
- c.execute("INSERT INTO sessions(ts,country,user_text,main_emotion)VALUES(?,?,?,?)",
183
- (datetime.utcnow().isoformat(timespec="seconds"), country, msg[:500], emotion))
184
  conn.commit()
185
  conn.close()
186
 
187
- # ---------------- Model training/loading ----------------
188
  def load_goemotions_dataset():
189
  ds = load_dataset("google-research-datasets/go_emotions", "simplified")
190
  return ds, ds["train"].features["labels"].feature.names
191
 
192
  def train_or_load_model():
193
- if os.path.isfile(MODEL_PATH):
194
  bundle = joblib.load(MODEL_PATH)
195
  if bundle.get("version") == MODEL_VERSION:
196
  return bundle["pipeline"], bundle["mlb"], bundle["label_names"]
@@ -200,8 +139,7 @@ def train_or_load_model():
200
  Y_train = mlb.fit_transform(y_train)
201
  clf = Pipeline([
202
  ("tfidf", TfidfVectorizer(lowercase=True, ngram_range=(1,2), min_df=2, max_df=0.9, strip_accents="unicode")),
203
- ("ovr", OneVsRestClassifier(
204
- LogisticRegression(solver="saga", max_iter=1000, class_weight="balanced"), n_jobs=-1))
205
  ])
206
  clf.fit(X_train, Y_train)
207
  joblib.dump({"version": MODEL_VERSION, "pipeline": clf, "mlb": mlb, "label_names": names}, MODEL_PATH)
@@ -210,121 +148,139 @@ def train_or_load_model():
210
  try:
211
  CLASSIFIER, MLB, LABEL_NAMES = train_or_load_model()
212
  except Exception as e:
213
- print("[WARN] Model not loaded:", e)
214
  CLASSIFIER, MLB, LABEL_NAMES = None, None, None
215
 
216
- # ---------------- Emotion detection ----------------
217
- def classify_text(text):
218
  if not CLASSIFIER: return []
219
- t = augment_text(text)
220
- proba = CLASSIFIER.predict_proba([t])[0]
221
- maxp = max(proba)
222
- thr = THRESHOLD if maxp >= THRESHOLD else max(MIN_THRESHOLD, maxp * 0.8)
223
- idxs = [i for i, p in enumerate(proba) if p >= thr] or [int(proba.argmax())]
224
  idxs.sort(key=lambda i: proba[i], reverse=True)
225
  return [(LABEL_NAMES[i], float(proba[i])) for i in idxs]
226
 
227
- def detect_emotions(text):
228
- chosen = classify_text(text)
229
- if not chosen: return "neutral"
 
230
  bucket = {}
231
- for lbl, p in chosen:
232
  app = GOEMO_TO_APP.get(lbl.lower(), "neutral")
233
- bucket[app] = max(bucket.get(app, 0), p)
234
  return max(bucket, key=bucket.get) if bucket else "neutral"
235
 
236
- # ---------------- Reply helpers ----------------
237
- def advice_for(emotion: str) -> str:
238
- return random.choice(SUGGESTIONS.get(emotion, SUGGESTIONS["neutral"]))
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
- # ---------------- Chat logic ----------------
241
  def crisis_block(country):
242
  msg = CRISIS_NUMBERS.get(country, CRISIS_NUMBERS["Other / Not listed"])
243
- return (f"💛 I'm really sorry you're feeling like this. You matter.\n\n"
244
- f"{msg}\n\nPlease reach out for help — you are not alone.")
245
-
246
- def chat_step(message, history, country, save_session):
247
- if CRISIS_RE.search(message):
248
- return crisis_block(country), "#FFD6E7", "neutral"
249
- if CLOSING_RE.search(message):
250
- emotion = detect_emotions(message)
251
- tip = advice_for(emotion)
252
- return (f"Thank you 💛 One last gentle reminder:\n\n• {tip}", "#FFFFFF", emotion)
253
-
254
- emotion = detect_emotions(message)
255
- color = COLOR_MAP.get(emotion, "#FFFFFF")
256
  if save_session:
257
- log_session(country, message, emotion)
258
- tip = advice_for(emotion)
259
- reply = f"• {tip}"
260
- if not history:
261
- reply += "\n\n*Can you tell me a bit more about that feeling?*"
262
- return reply, color, emotion
263
-
264
- # ---------------- Interface ----------------
265
- init_db()
266
 
267
- custom_css = """
268
- :root, body, .gradio-container { transition: background-color 0.8s ease !important; }
269
- .typing { font-style: italic; opacity: 0.8; animation: blink 1s infinite; }
270
- @keyframes blink { 50% {opacity: 0.4;} }
271
- """
272
 
273
- with gr.Blocks(css=custom_css, title="🪞 MoodMirror+ (Advice-only)") as demo:
274
  style = gr.HTML("")
275
- gr.Markdown("### 🪞 MoodMirror+ — Gentle Emotional Support 🌿\n"
276
- "Always responds with a caring self-care tip.\n\n"
277
- "_Not medical advice. If you feel unsafe, please reach out for help immediately._")
278
-
279
- with gr.Row():
280
- country = gr.Dropdown(list(CRISIS_NUMBERS.keys()), value="Other / Not listed", label="Country")
281
- save_ok = gr.Checkbox(False, label="Save anonymized session (no personal data)")
282
-
283
- chat = gr.Chatbot(height=360)
284
- msg = gr.Textbox(label="Your message", placeholder="Tell me how you feel...")
285
- with gr.Row():
286
- send = gr.Button("Send", variant="primary")
287
- regen = gr.Button("🔁 New advice", variant="secondary")
288
-
289
- typing = gr.Markdown("", elem_classes="typing")
290
- last_emotion = gr.State("neutral") # store the last detected emotion
291
-
292
- def respond(user_msg, chat_hist, country_choice, save_flag, _last_emotion):
293
- if not user_msg or not user_msg.strip():
294
- return chat_hist + [[user_msg, "Please share a short sentence about how you feel 🙂"]], "", "", "", _last_emotion
295
- # normal chat step
296
- reply, color, emotion = chat_step(user_msg, chat_hist, country_choice, bool(save_flag))
297
- style_tag = f"<style>:root,body,.gradio-container{{background:{color}!important;}}</style>"
298
- return chat_hist + [[user_msg, reply]], "", style_tag, "", emotion
299
-
300
- def draw_new_advice(chat_hist, _last_emotion):
301
- # If no previous emotion, nudge user
302
- if not _last_emotion:
303
- return chat_hist + [[None, "Please send a message first so I can sense your mood."]], "", "", _last_emotion
304
- # Generate a fresh tip for the stored emotion
305
- tip = advice_for(_last_emotion)
306
- reply = f"• {tip}"
307
- return chat_hist + [[None, reply]], "", "", _last_emotion
308
-
309
- # Wire buttons
310
- send.click(
311
- respond,
312
- inputs=[msg, chat, country, save_ok, last_emotion],
313
- outputs=[chat, typing, style, msg, last_emotion],
314
- queue=True
315
- )
316
- msg.submit(
317
- respond,
318
- inputs=[msg, chat, country, save_ok, last_emotion],
319
- outputs=[chat, typing, style, msg, last_emotion],
320
- queue=True
321
- )
322
- regen.click(
323
- draw_new_advice,
324
- inputs=[chat, last_emotion],
325
- outputs=[chat, typing, style, last_emotion],
326
- queue=True
327
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
 
329
  if __name__ == "__main__":
330
  demo.queue()
 
1
  # ================================
2
+ # 🪞 MoodMirror+ — Text Emotion • Advice-only + Emergency Numbers tab
3
+ # - Text: GoEmotions (TF-IDF + OneVsRest LR, dataset-only)
4
+ # - Adds a "Numéros d'urgence" tab for crisis helplines by country
5
  # ================================
6
  import os
7
  import re
8
  import random
9
  import sqlite3
10
  import joblib
11
+ import numpy as np
12
  from datetime import datetime
13
 
14
  import gradio as gr
 
18
  from sklearn.linear_model import LogisticRegression
19
  from sklearn.multiclass import OneVsRestClassifier
20
  from sklearn.pipeline import Pipeline
 
21
 
22
  # ---------------- Storage paths ----------------
23
  def _pick_data_dir():
 
25
  return "/data"
26
  return os.getcwd()
27
 
28
+ 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 = "v11-text-only-intro-reason"
33
 
34
+ # ---------------- Crisis & closing ----------------
35
  CRISIS_RE = re.compile(
36
  r"\b(self[- ]?harm|suicid|kill myself|end my life|overdose|cutting|i don.?t want to live|can.?t go on)\b",
37
  re.I,
 
42
  )
43
 
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
  # ---------------- Advice library ----------------
54
  SUGGESTIONS = {
55
+ "sadness": ["Go for a 5-minute outside walk and name three colors you see."],
56
+ "fear": ["Do 5-4-3-2-1 grounding: 5 see, 4 feel, 3 hear, 2 smell, 1 taste."],
57
+ "anger": ["Take space before replying; set a 10-minute timer."],
58
+ "nervousness": ["4-7-8 breathing: in 4s, hold 7s, out 8s (four rounds)."],
59
+ "boredom": ["Set a 2-minute timer and start anything small."],
60
+ "grief": ["Hold a photo or object and say their name softly."],
61
+ "love": ["Send a kind message without expecting a reply."],
62
+ "joy": ["Pause and take three slow breaths to savor this."],
63
+ "curiosity": ["Search one concept and read just the first paragraph."],
64
+ "gratitude": ["List three tiny things that made today easier."],
65
+ "neutral": ["Take one slow breath and relax your hands."],
66
+ }
67
+
68
+ WHY_BY_EMOTION = {
69
+ "sadness": "Small sensory and connection cues can ease low mood.",
70
+ "fear": "Grounding + longer exhales calm the threat system.",
71
+ "anger": "Space + movement lower adrenaline to respond, not react.",
72
+ "nervousness": "Slow breathing and micro-actions reduce anxious energy.",
73
+ "boredom": "Novelty and small starts re-engage attention.",
74
+ "grief": "Rituals and gentle care help carry love and loss.",
75
+ "love": "Expressing care strengthens bonds and self-kindness.",
76
+ "joy": "Savoring and sharing consolidate positive memories.",
77
+ "curiosity": "Small explorations feed learning and perspective.",
78
+ "gratitude": "Noticing support shifts attention toward strengths.",
79
+ "neutral": "Simple body care keeps your baseline steady.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  }
81
 
82
  COLOR_MAP = {
 
84
  "sadness": "#BBDEFB", "grief": "#B3E5FC",
85
  "fear": "#E1BEE7", "nervousness": "#E1BEE7",
86
  "anger": "#FFCCBC", "boredom": "#E0E0E0",
87
+ "neutral": "#F5F5F5", "curiosity": "#E6EE9C",
88
  }
89
 
90
+ GOEMO_TO_APP = {"sadness": "sadness", "joy": "joy", "fear": "fear", "anger": "anger", "neutral": "neutral"}
91
+
92
+ # ---------------- Helpers ----------------
93
+ THRESHOLD_BASE = 0.30
94
+ MIN_THRESHOLD = 0.10
95
+ CLEAN_RE = re.compile(r"(https?://\S+)|(@\w+)|(#\w+)|[^a-zA-Z0-9\s']")
 
 
 
 
96
 
97
+ def clean_text(s: str) -> str:
98
+ s = s.lower()
99
+ s = CLEAN_RE.sub(" ", s)
100
+ s = re.sub(r"\s+", " ", s).strip()
101
+ return s
102
+
103
+ def augment_text(text: str, history=None) -> str:
104
+ return clean_text(text or "")
105
+
106
+ # ---------------- SQLite ----------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  def get_conn():
108
  return sqlite3.connect(DB_PATH, check_same_thread=False, timeout=10)
109
 
110
  def init_db():
111
  conn = get_conn()
112
+ conn.execute("""CREATE TABLE IF NOT EXISTS sessions(
 
113
  id INTEGER PRIMARY KEY AUTOINCREMENT,
114
+ ts TEXT, country TEXT, user_text TEXT, main_emotion TEXT
115
+ )""")
116
  conn.commit()
117
  conn.close()
118
 
119
  def log_session(country, msg, emotion):
120
  conn = get_conn()
121
+ conn.execute("INSERT INTO sessions(ts,country,user_text,main_emotion)VALUES(?,?,?,?)",
122
+ (datetime.utcnow().isoformat(timespec='seconds'), country, (msg or "")[:500], emotion))
 
123
  conn.commit()
124
  conn.close()
125
 
126
+ # ---------------- Model ----------------
127
  def load_goemotions_dataset():
128
  ds = load_dataset("google-research-datasets/go_emotions", "simplified")
129
  return ds, ds["train"].features["labels"].feature.names
130
 
131
  def train_or_load_model():
132
+ if os.path.exists(MODEL_PATH):
133
  bundle = joblib.load(MODEL_PATH)
134
  if bundle.get("version") == MODEL_VERSION:
135
  return bundle["pipeline"], bundle["mlb"], bundle["label_names"]
 
139
  Y_train = mlb.fit_transform(y_train)
140
  clf = Pipeline([
141
  ("tfidf", TfidfVectorizer(lowercase=True, ngram_range=(1,2), min_df=2, max_df=0.9, strip_accents="unicode")),
142
+ ("ovr", OneVsRestClassifier(LogisticRegression(solver="saga", max_iter=1000, class_weight="balanced"), n_jobs=-1))
 
143
  ])
144
  clf.fit(X_train, Y_train)
145
  joblib.dump({"version": MODEL_VERSION, "pipeline": clf, "mlb": mlb, "label_names": names}, MODEL_PATH)
 
148
  try:
149
  CLASSIFIER, MLB, LABEL_NAMES = train_or_load_model()
150
  except Exception as e:
151
+ print("[ERROR] Model load/train:", e)
152
  CLASSIFIER, MLB, LABEL_NAMES = None, None, None
153
 
154
+ def classify_text(text_augmented: str):
 
155
  if not CLASSIFIER: return []
156
+ proba = CLASSIFIER.predict_proba([text_augmented])[0]
157
+ max_p = float(np.max(proba)) if len(proba) else 0.0
158
+ thr = max(MIN_THRESHOLD, THRESHOLD_BASE * max_p + 0.15)
159
+ idxs = [i for i, p in enumerate(proba) if p >= thr] or [int(np.argmax(proba))]
 
160
  idxs.sort(key=lambda i: proba[i], reverse=True)
161
  return [(LABEL_NAMES[i], float(proba[i])) for i in idxs]
162
 
163
+ def detect_emotion_text(message: str, history):
164
+ labels = classify_text(augment_text(message, history))
165
+ if not labels:
166
+ return "neutral"
167
  bucket = {}
168
+ for lbl, p in labels:
169
  app = GOEMO_TO_APP.get(lbl.lower(), "neutral")
170
+ bucket[app] = max(bucket.get(app, 0.0), p)
171
  return max(bucket, key=bucket.get) if bucket else "neutral"
172
 
173
+ # ---------------- Advice logic ----------------
174
+ def pick_advice_from_pool(emotion: str, pool: dict, last_tip: str = ""):
175
+ tips_all = SUGGESTIONS.get(emotion, SUGGESTIONS["neutral"])
176
+ entry = pool.get(emotion, {"unused": [], "last": ""})
177
+ if not entry["unused"]:
178
+ refill = [t for t in tips_all if t != entry.get("last","")] or tips_all[:]
179
+ random.shuffle(refill)
180
+ entry["unused"] = refill
181
+ tip = entry["unused"].pop(0)
182
+ entry["last"] = tip
183
+ pool[emotion] = entry
184
+ return tip, pool
185
+
186
+ def format_reply(emotion: str, tip: str) -> str:
187
+ why = WHY_BY_EMOTION.get(emotion, WHY_BY_EMOTION["neutral"])
188
+ return f"Try this now:\n• {tip}\n_(Why it helps: {why})_"
189
 
 
190
  def crisis_block(country):
191
  msg = CRISIS_NUMBERS.get(country, CRISIS_NUMBERS["Other / Not listed"])
192
+ return f"💛 You matter. If you're in danger or thinking of harming yourself, please reach out now.\n\n{msg}"
193
+
194
+ def chat_step(user_text, history, country, save_session, advice_pool):
195
+ if user_text and CRISIS_RE.search(user_text):
196
+ return crisis_block(country), "#FFD6E7", "neutral", "", advice_pool
197
+ if user_text and CLOSING_RE.search(user_text):
198
+ emotion = "neutral"
199
+ tip, advice_pool = pick_advice_from_pool(emotion, advice_pool)
200
+ reply = format_reply(emotion, tip)
201
+ return reply, "#FFFFFF", emotion, tip, advice_pool
202
+ emotion = detect_emotion_text(user_text or "", history)
203
+ color = COLOR_MAP.get(emotion, "#F5F5F5")
 
204
  if save_session:
205
+ log_session(country, user_text or "", emotion)
206
+ tip, advice_pool = pick_advice_from_pool(emotion, advice_pool)
207
+ reply = format_reply(emotion, tip)
208
+ return reply, color, emotion, tip, advice_pool
 
 
 
 
 
209
 
210
+ # ---------------- UI ----------------
211
+ init_db()
 
 
 
212
 
213
+ with gr.Blocks(title="🪞 MoodMirror+ — Text Emotion • Advice-only") as demo:
214
  style = gr.HTML("")
215
+ gr.Markdown("### 🪞 MoodMirror+ — Emotion-aware advice\n_Not medical advice. If unsafe, please reach out for help._")
216
+
217
+ with gr.Tabs():
218
+ # Tab 1 — Conseils
219
+ with gr.Tab("Conseils"):
220
+ with gr.Row():
221
+ country = gr.Dropdown(list(CRISIS_NUMBERS.keys()), value="Other / Not listed", label="Country")
222
+ save_ok = gr.Checkbox(False, label="Save anonymized session")
223
+
224
+ chat = gr.Chatbot(height=380)
225
+ msg = gr.Textbox(label="Your message", placeholder="Share how you feel...")
226
+
227
+ with gr.Row():
228
+ send = gr.Button("Send", variant="primary")
229
+ regen = gr.Button("🔁 New advice", variant="secondary")
230
+
231
+ last_emotion = gr.State("neutral")
232
+ last_tip = gr.State("")
233
+ advice_pool = gr.State({})
234
+
235
+ def respond(user_msg, chat_hist, country_choice, save_flag, _emotion, _tip, _pool):
236
+ if not user_msg or not user_msg.strip():
237
+ return chat_hist + [[user_msg, "Please share how you feel 🙂"]], "", _emotion, _tip, _pool
238
+ reply, color, emotion, tip, _pool = chat_step(user_msg, chat_hist, country_choice, bool(save_flag), _pool)
239
+ style_tag = f"<style>:root,body,.gradio-container{{background:{color}!important;}}</style>"
240
+ return chat_hist + [[user_msg, reply]], style_tag, emotion, tip, _pool
241
+
242
+ def new_advice(chat_hist, _emotion, _tip, _pool):
243
+ tip, _pool = pick_advice_from_pool(_emotion, _pool, last_tip=_tip)
244
+ reply = format_reply(_emotion, tip)
245
+ return chat_hist + [[None, reply]], "", _emotion, tip, _pool
246
+
247
+ send.click(respond,
248
+ inputs=[msg, chat, country, save_ok, last_emotion, last_tip, advice_pool],
249
+ outputs=[chat, style, last_emotion, last_tip, advice_pool],
250
+ queue=True)
251
+ msg.submit(respond,
252
+ inputs=[msg, chat, country, save_ok, last_emotion, last_tip, advice_pool],
253
+ outputs=[chat, style, last_emotion, last_tip, advice_pool],
254
+ queue=True)
255
+ regen.click(new_advice,
256
+ inputs=[chat, last_emotion, last_tip, advice_pool],
257
+ outputs=[chat, style, last_emotion, last_tip, advice_pool],
258
+ queue=True)
259
+
260
+ # Tab 2 — Numéros d'urgence
261
+ with gr.Tab("Numéros d'urgence"):
262
+ gr.Markdown(
263
+ "#### 📟 Numéros d’urgence par pays\n"
264
+ "Sélectionne ton pays pour voir la ligne d’aide recommandée.\n"
265
+ "En cas de danger immédiat, compose **112** ou **911** selon ton pays."
266
+ )
267
+
268
+ country_view = gr.Dropdown(
269
+ choices=list(CRISIS_NUMBERS.keys()),
270
+ value="France" if "France" in CRISIS_NUMBERS else list(CRISIS_NUMBERS.keys())[0],
271
+ label="Pays"
272
+ )
273
+ crisis_info = gr.Markdown(value=crisis_block("France") if "France" in CRISIS_NUMBERS else crisis_block("Other / Not listed"))
274
+
275
+ def show_crisis_for_country(c):
276
+ return crisis_block(c)
277
+
278
+ country_view.change(show_crisis_for_country, inputs=country_view, outputs=crisis_info)
279
+
280
+ gr.Markdown(
281
+ "> ℹ️ Ces numéros sont fournis à titre informatif. "
282
+ "Si ton pays n’est pas listé, contacte les services d’urgence locaux (**112/911**)."
283
+ )
284
 
285
  if __name__ == "__main__":
286
  demo.queue()