""" app.py โ€” Undertone The signal beneath the surface. Mental Health Triage Assistant ยท Gradio interface for HuggingFace Spaces """ import torch import gradio as gr from pathlib import Path from transformers import ( DistilBertTokenizerFast, DistilBertForSequenceClassification, ) MODEL_DIR = Path(__file__).parent / "model" / "saved" LABEL2ID = {"informational": 0, "emotional_support": 1, "escalation": 2} ID2LABEL = {v: k for k, v in LABEL2ID.items()} LABEL_CONFIG = { "informational": { "emoji": "๐ŸŸข", "title": "Informational", "response": ( "This message appears to be asking for information or resources. " "We can help point you to reliable mental health information and services." ), "action": "Provide relevant resources and psychoeducation.", }, "emotional_support": { "emoji": "๐ŸŸก", "title": "Emotional Support Needed", "response": ( "It sounds like you're going through a difficult time. " "You're not alone โ€” a counselor is here to listen and support you." ), "action": "Connect with a counselor for empathetic listening and coping support.", }, "escalation": { "emoji": "๐Ÿ”ด", "title": "Immediate Attention Required", "response": ( "Your message suggests you may be in crisis. " "Please reach out immediately โ€” help is available right now." ), "action": "Escalate to crisis counselor. Provide crisis hotline numbers immediately.", "crisis_resources": [ "๐Ÿ†˜ 988 Suicide & Crisis Lifeline โ€” Call or text **988**", "๐Ÿ’ฌ Crisis Text Line โ€” Text HOME to **741741**", "๐ŸŒ International resources โ€” https://www.iasp.info/resources/Crisis_Centres/", ], }, } # โ”€โ”€ ้ซ˜ๅฑ emoji โ†’ ๆ–‡ๅญ—ๆ˜ ๅฐ„ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # DistilBERT tokenizer ๆŠŠ emoji ๅ˜ๆˆ [UNK]๏ผŒ้ข„ๅค„็†่ฝฌๆˆๆ–‡ๅญ—ไฟ็•™่ฏญไน‰ EMOJI_RISK_MAP = { # ๆ˜Ž็กฎๅฑๆœบไฟกๅท "๐Ÿ”ช": " knife weapon harm ", "๐Ÿ’Š": " pills medication overdose ", "๐Ÿ”ซ": " gun weapon ", "๐Ÿช“": " weapon harm ", "โšฐ๏ธ": " death coffin ", "๐Ÿชฆ": " grave death ", "โ˜ ๏ธ": " death skull ", "๐Ÿ’€": " death dying ", "๐Ÿฉธ": " blood injury ", # ๅผบ็ƒˆๆƒ…็ปชไฟกๅท "๐Ÿ˜ญ": " crying devastated ", "๐Ÿ˜ข": " crying sad ", "๐Ÿ†˜": " emergency help urgent ", "๐Ÿ˜ถ": " numb empty ", "๐Ÿ’”": " heartbroken pain ", "๐Ÿฅบ": " desperate pleading ", "๐Ÿ˜ซ": " exhausted overwhelmed ", "๐Ÿ˜ฉ": " hopeless drained ", "๐ŸŒŠ": " drowning overwhelmed ", "๐Ÿ–ค": " darkness despair ", "๐Ÿ˜”": " sad down ", "๐Ÿ˜ž": " disappointed hopeless ", "๐Ÿ™ƒ": " ironic resigned ", } def preprocess(text: str) -> str: """ๆŠŠ้ซ˜ๅฑ emoji ๆ›ฟๆขไธบๅฏนๆจกๅž‹ๆœ‰ๆ„ไน‰็š„ๆ–‡ๅญ—""" for emoji_char, replacement in EMOJI_RISK_MAP.items(): text = text.replace(emoji_char, replacement) return " ".join(text.split()) # ๆธ…็†ๅคšไฝ™็ฉบๆ ผ DEVICE = ( "mps" if torch.backends.mps.is_available() else "cuda" if torch.cuda.is_available() else "cpu" ) # โ”€โ”€ ๆจกๅž‹ๅŠ ่ฝฝ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ print(f"Loading model from {MODEL_DIR} on {DEVICE}...") tokenizer = DistilBertTokenizerFast.from_pretrained(str(MODEL_DIR)) model = DistilBertForSequenceClassification.from_pretrained(str(MODEL_DIR)).to(DEVICE) model.eval() print("Model loaded.") @torch.no_grad() def triage(message: str): if not message or not message.strip(): return ( "**Please enter a message to analyze.**", {}, "โ€”", "โ€”", ) # emoji ้ข„ๅค„็† processed = preprocess(message.strip()) enc = tokenizer( [processed], truncation=True, padding=True, max_length=128, return_tensors="pt", ) logits = model( input_ids=enc["input_ids"].to(DEVICE), attention_mask=enc["attention_mask"].to(DEVICE), ).logits probs = torch.softmax(logits, dim=-1).cpu().numpy()[0] label_id = int(probs.argmax()) label_name = ID2LABEL[label_id] config = LABEL_CONFIG[label_name] confidence = float(probs[label_id]) prob_dict = { LABEL_CONFIG[ID2LABEL[i]]["title"]: float(probs[i]) for i in range(3) } classification_md = ( f"## {config['emoji']} {config['title']}\n\n" f"**Confidence:** {confidence:.1%}\n\n" f"---\n\n" f"{config['response']}" ) crisis_md = "" if label_name == "escalation": crisis_md = "### ๐Ÿ†˜ Crisis Resources\n\n" + "\n\n".join(config["crisis_resources"]) else: crisis_md = "_No immediate crisis resources needed._" return ( classification_md, prob_dict, config["action"], crisis_md, ) # โ”€โ”€ ็คบไพ‹ๆถˆๆฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ EXAMPLES = [ ["What are the signs of anxiety disorder?"], ["I've been feeling really low lately and don't know what to do ๐Ÿ˜”"], ["idk i just want everything to stop ngl"], ["i fel so aloen and its getting worse"], ["๐Ÿ˜ญ๐Ÿ”ช cant do this anymore"], ["lol whatever doesn't matter anyway ๐Ÿ’€"], ["Where can I find free therapy in my area?"], ["I've been giving away my stuff to friends lately"], ] CUSTOM_CSS = """ :root { --bg: #0F1117; --surface: #1A1D27; --border: #2A2D3A; --text: #E8EAF0; --muted: #6B7280; } body, .gradio-container { background: var(--bg) !important; color: var(--text) !important; font-family: 'Inter', system-ui, sans-serif !important; } .app-header { text-align: center; padding: 2rem 1rem 1rem; border-bottom: 1px solid var(--border); margin-bottom: 1.5rem; } .app-header h1 { font-size: 1.8rem; font-weight: 700; color: #fff; margin: 0; } .app-header p { color: var(--muted); font-size: 0.9rem; margin-top: 0.4rem; } .badge { display: inline-block; padding: 2px 10px; border-radius: 99px; font-size: 0.7rem; font-weight: 600; text-transform: uppercase; background: #2A2D3A; color: #9CA3AF; margin-top: 0.5rem; } .disclaimer { background: #1E1B2E; border: 1px solid #3B2F6B; border-radius: 8px; padding: 0.75rem 1rem; font-size: 0.8rem; color: #9CA3AF; margin-bottom: 1.5rem; } .label-legend { display: flex; gap: 1.5rem; justify-content: center; padding: 0.75rem; border-top: 1px solid var(--border); margin-top: 1.5rem; font-size: 0.8rem; color: var(--muted); } .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 5px; } """ # โ”€โ”€ ็•Œ้ข โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ with gr.Blocks(css=CUSTOM_CSS, title="Undertone") as demo: gr.HTML("""

๐Ÿง  Undertone

The signal beneath the surface โ€” mental health message triage

Research Demo ยท DistilBERT Fine-Tuned
""") gr.HTML("""
โš ๏ธ Disclaimer: This is a research prototype for educational purposes only. It does not provide medical advice and should never replace professional mental health support. If you or someone you know is in crisis, please contact a crisis service immediately.
""") with gr.Row(): with gr.Column(scale=1): msg_input = gr.Textbox( label="Message", placeholder="Type a support message here...", lines=4, max_lines=8, ) submit_btn = gr.Button("Analyze Message", variant="primary") gr.Examples(examples=EXAMPLES, inputs=msg_input, label="Try an example") with gr.Column(scale=1): classification_out = gr.Markdown(label="Classification") confidence_out = gr.Label(label="Confidence Scores", num_top_classes=3) with gr.Row(): with gr.Column(): action_out = gr.Textbox(label="Recommended Action", interactive=False) with gr.Column(): crisis_out = gr.Markdown(label="Crisis Resources") gr.HTML("""
Informational Emotional Support Escalation
""") submit_btn.click( fn=triage, inputs=msg_input, outputs=[classification_out, confidence_out, action_out, crisis_out], ) msg_input.submit( fn=triage, inputs=msg_input, outputs=[classification_out, confidence_out, action_out, crisis_out], ) if __name__ == "__main__": demo.launch()