cassandrasestier commited on
Commit
d1fb552
ยท
verified ยท
1 Parent(s): cfe8e58

Update app.py

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