import streamlit as st import torch import torch.nn as nn import torch.nn.functional as F from transformers import RobertaTokenizerFast, RobertaModel import re import os # ── Page config ────────────────────────────────────────────────────────────── st.set_page_config( page_title="Sarcasm Detector", page_icon="🙄", layout="centered" ) # ── Model Definition (must match training) ─────────────────────────────────── class SarcasmDetector(nn.Module): def __init__(self, model_name="roberta-base", num_sarc=2, num_sent=3, dropout=0.15): super().__init__() self.roberta = RobertaModel.from_pretrained(model_name) H = self.roberta.config.hidden_size # 768 self.sarcasm_head = nn.Sequential( nn.Dropout(dropout), nn.Linear(H, 256), nn.GELU(), nn.Dropout(dropout), nn.Linear(256, num_sarc)) self.sentiment_head = nn.Sequential( nn.Dropout(dropout), nn.Linear(H, 256), nn.GELU(), nn.Dropout(dropout), nn.Linear(256, num_sent)) self.confidence_gate = nn.Linear(H, 1) def forward(self, input_ids, attention_mask): out = self.roberta(input_ids=input_ids, attention_mask=attention_mask) cls = out.last_hidden_state[:, 0, :] return (self.sarcasm_head(cls), self.sentiment_head(cls), torch.sigmoid(self.confidence_gate(cls)), None) # ── Constants ───────────────────────────────────────────────────────────────── SARCASM_ID2LABEL = {0: "not_sarcastic", 1: "sarcastic"} SENTIMENT_ID2LABEL = {0: "negative", 1: "neutral", 2: "positive"} MAX_LEN = 128 SARCASM_THRESH = 0.45 # matches notebook CFG.SARCASM_THRESH DEVICE = torch.device("cpu") # ── Load model (cached) ─────────────────────────────────────────────────────── @st.cache_resource def load_model(): tokenizer = RobertaTokenizerFast.from_pretrained("roberta-base") model = SarcasmDetector("roberta-base").to(DEVICE) ckpt_path = "best_model.pt" if os.path.exists(ckpt_path): try: state_dict = torch.load(ckpt_path, map_location=DEVICE, weights_only=False) model.load_state_dict(state_dict, strict=True) model.eval() return tokenizer, model, True, "✅ Model loaded successfully!" except Exception as e: return tokenizer, model, False, f"❌ Load error: {e}" return tokenizer, model, False, f"❌ File not found: {ckpt_path} | Files: {os.listdir('.')}" def clean_text(text): return re.sub(r"\s+", " ", str(text)).strip() @torch.no_grad() def predict(text, tokenizer, model): text = clean_text(text) enc = tokenizer(text, max_length=MAX_LEN, padding="max_length", truncation=True, return_tensors="pt") sar_l, sen_l, conf, _ = model( enc["input_ids"].to(DEVICE), enc["attention_mask"].to(DEVICE) ) sar_p = F.softmax(sar_l, dim=1)[0].cpu().numpy() sen_p = F.softmax(sen_l, dim=1)[0].cpu().numpy() conf_v = conf[0].item() # ── Conversational sarcasm heuristic boost ──────────────────────────── CONVERSATIONAL_TRIGGERS = [ "best day ever", "oh great", "just what i needed", "wow thanks", "oh wonderful", "fantastic news", "oh perfect", "love it when", "because that's totally", "yeah right", "sure it is", "oh sure", "oh absolutely", "what could go wrong", "totally fine", "great idea", "brilliant idea", "oh brilliant", "oh how lovely", "how lovely", "oh joy", "oh lovely", "oh nice", "oh how nice", "oh how great", "best thing ever", "love mondays", "love traffic", "love waiting", "so fun", "so exciting", "oh can't wait", "my favourite", "my favorite", "best part", "best news", ] text_lower = text.lower() triggered = any(t in text_lower for t in CONVERSATIONAL_TRIGGERS) if triggered and sar_p[1] < 0.60: boost = 0.55 + (0.10 * sar_p[1]) sar_p[1] = max(sar_p[1], boost) sar_p[0] = 1.0 - sar_p[1] # ───────────────────────────────────────────────────────────────────── sar_id = 1 if sar_p[1] >= SARCASM_THRESH else 0 sen_id = int(sen_p.argmax()) # Sarcasm-aware sentiment gate if sar_id == 1 and sar_p[1] > 0.65: final_sentiment = "implicit_negative ⚠️" else: final_sentiment = SENTIMENT_ID2LABEL[sen_id] return { "sarcasm_label": SARCASM_ID2LABEL[sar_id], "sarcasm_prob": float(sar_p[1]), "not_sarc_prob": float(sar_p[0]), "sentiment": final_sentiment, "confidence": float(conf_v), "sen_probs": sen_p.tolist(), } # ── UI ──────────────────────────────────────────────────────────────────────── st.markdown("""
Multi-Task RoBERTa · F1-Macro 0.977 · AUC 0.997 · Sentiment-Incongruence Framework
""", unsafe_allow_html=True) st.markdown("---") # Load model tokenizer, model, model_loaded, load_msg = load_model() st.caption(load_msg) if not model_loaded: st.error("⚠️ Model weights not loaded correctly. Check logs above.") st.stop() # Input st.markdown("### 📝 Enter Text") user_input = st.text_area( label="", placeholder='e.g. "Study finds that watching TV all day is actually good for your health"', height=120 ) # Example buttons st.markdown("**Try an example:**") col1, col2, col3 = st.columns(3) with col1: if st.button("😏 Headline Sarcasm"): user_input = "Study Confirms That Working 80 Hours A Week Is The Key To A Fulfilling Life" with col2: if st.button("😏 Casual Sarcasm"): user_input = "Oh great, my flight got delayed again. Best day ever." with col3: if st.button("😊 Not Sarcastic"): user_input = "Dog reunited with owner after being missing for three days." # Predict if st.button("🔍 Analyse", type="primary", use_container_width=True): if not user_input.strip(): st.warning("Please enter some text first!") else: with st.spinner("Analysing..."): result = predict(user_input, tokenizer, model) st.markdown("---") st.markdown("### 📊 Results") # Sarcasm result is_sarc = result["sarcasm_label"] == "sarcastic" sarc_color = "#e74c3c" if is_sarc else "#2ecc71" sarc_emoji = "😏 SARCASTIC" if is_sarc else "😊 NOT SARCASTIC" sarc_pct = result["sarcasm_prob"] * 100 st.markdown(f"""Sarcasm probability: {sarc_pct:.1f}% | Confidence: {result['confidence']*100:.1f}%
{sent_emoji} Sentiment: {sent.upper()}