Spaces:
Sleeping
Sleeping
| import re | |
| import torch | |
| import nltk | |
| from nltk.sentiment.vader import SentimentIntensityAnalyzer | |
| from transformers import RobertaTokenizer, RobertaForSequenceClassification | |
| # Ensure VADER lexicon is downloaded | |
| try: | |
| nltk.data.find('sentiment/vader_lexicon.zip') | |
| except LookupError: | |
| nltk.download('vader_lexicon', quiet=True) | |
| # Hyperbolic / idiomatic phrases commonly used in sarcastic replies | |
| HYPERBOLIC_PATTERNS = [ | |
| r"wouldn't miss it", r"for the world", r"nothing i enjoy more", | |
| r"absolutely love", r"my favorite thing", r"oh i'm sure", | |
| r"what a surprise", r"how wonderful", r"how delightful", | |
| r"so thrilled", r"couldn't be happier", r"just wonderful", | |
| r"oh really", r"oh definitely", r"oh absolutely", | |
| r"wouldn't dream of", r"nothing better", r"love nothing more", | |
| r"can hardly wait", r"what could go wrong", r"story of my life", | |
| r"lucky me", r"just my luck", r"oh joy", r"how exciting", | |
| r"what a treat", r"how original", r"so original", | |
| r"oh great", r"yeah right", r"sure thing", | |
| r"absolutely perfect", r"just perfect", r"couldn't be better", | |
| r"what a pleasure", r"so helpful", r"thanks a lot", | |
| ] | |
| # Patterns indicating genuine empathy/support β used to DAMPEN false positives | |
| GENUINE_MARKERS = [ | |
| r"that must.ve been", r"that must have been", | |
| r"that's (rough|tough|unfortunate|hard|stressful)", | |
| r"don't worry", r"happens sometimes", r"sorry to hear", | |
| r"that (sucks|stinks)", r"hope you", r"i'm sorry", | |
| r"that's (manageable|okay|understandable|good|great|nice)", | |
| r"how did .+ go", r"are you (ok|okay|alright)", | |
| r"hang in there", r"take it easy", r"it'll be", | |
| r"that's really (unlucky|unfortunate|bad|sad)", | |
| r"sounds like it didn't", | |
| ] | |
| class SarcasmPredictor: | |
| """Loads the fine-tuned RoBERTa model and predicts sarcasm scores.""" | |
| def __init__(self, model_path: str = "models/roberta_model"): | |
| self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| self.tokenizer = RobertaTokenizer.from_pretrained(model_path) | |
| self.model = RobertaForSequenceClassification.from_pretrained(model_path) | |
| self.model.to(self.device) | |
| self.model.eval() | |
| self.sia = SentimentIntensityAnalyzer() | |
| def predict(self, text: str, context: str = "", | |
| speaker_prior_scores: list = None) -> dict: | |
| """Predict sarcasm probability with multi-signal post-processing.""" | |
| # ββ 1. Model Inference (unchanged β uses trained format) ββ | |
| if context: | |
| formatted_input = f"A: {context} [SEP] B: {text}" | |
| else: | |
| formatted_input = f"B: {text}" | |
| inputs = self.tokenizer( | |
| formatted_input, | |
| padding="max_length", | |
| truncation=True, | |
| max_length=96, | |
| return_tensors="pt" | |
| ).to(self.device) | |
| with torch.no_grad(): | |
| outputs = self.model(**inputs) | |
| probs = torch.softmax(outputs.logits, dim=1) | |
| sarcasm_prob = probs[0][1].item() | |
| raw_score = sarcasm_prob # preserve for speaker history | |
| # ββ 2. Boosting Heuristics ββββββββββββββββββββββββββββββββ | |
| msg_sentiment = self.sia.polarity_scores(text)['compound'] | |
| # Normalize curly quotes to straight so regex patterns match | |
| text_lower = text.lower().replace('\u2019', "'").replace('\u2018', "'") | |
| boost = 0.0 | |
| signals = 0 | |
| # Compute context sentiment once (used by both boosting & dampening) | |
| ctx_sentiment = 0.0 | |
| if context: | |
| ctx_sentiment = self.sia.polarity_scores(context)['compound'] | |
| # 2a. Sentiment Contrast (strong only β negative ctx β positive reply) | |
| if context and ctx_sentiment < -0.2 and msg_sentiment > 0.4: | |
| boost += 0.15 | |
| signals += 1 | |
| # 2b. Hyperbolic / Idiomatic Language Detection | |
| has_hyperbole = False | |
| if context: | |
| for pattern in HYPERBOLIC_PATTERNS: | |
| if re.search(pattern, text_lower): | |
| has_hyperbole = True | |
| boost += 0.25 | |
| signals += 1 | |
| break | |
| # 2c. Same-Speaker Contradiction (uses RAW model scores) | |
| if speaker_prior_scores: | |
| if has_hyperbole and any(s > 0.5 for s in speaker_prior_scores): | |
| boost += 0.20 | |
| signals += 1 | |
| # 2d. Compound boost β multiple signals reinforce each other | |
| if signals >= 2: | |
| boost += 0.10 | |
| sarcasm_prob = min(0.99, sarcasm_prob + boost) | |
| # ββ 3. Dampening β reduce false positives βββββββββββββββββ | |
| # 3a. Questions without sarcastic patterns are usually genuine | |
| is_question = text.rstrip().endswith("?") | |
| if is_question and not has_hyperbole: | |
| sarcasm_prob *= 0.4 | |
| # 3b. Empathetic / supportive language | |
| if not has_hyperbole: | |
| for pattern in GENUINE_MARKERS: | |
| if re.search(pattern, text_lower): | |
| sarcasm_prob *= 0.3 | |
| break | |
| # 3c. First-person negative = complaint, not sarcasm | |
| # "I had never seen before", "I think I might not get selected" | |
| if not has_hyperbole and msg_sentiment < 0.1: | |
| if re.search(r'\b(i|my|me|myself)\b', text_lower): | |
| sarcasm_prob *= 0.35 | |
| # 3d. Negative-on-negative = empathy/agreement, not sarcasm | |
| # Sarcasm needs CONTRAST (positive words in negative context). | |
| # When context is negative and reply isn't clearly positive, | |
| # it's genuine commiseration, not sarcasm. | |
| if context and not has_hyperbole: | |
| if ctx_sentiment < -0.1 and msg_sentiment < 0.2: | |
| sarcasm_prob *= 0.3 | |
| # 3e. No strong positive words = unlikely sarcasm | |
| # Sarcasm almost always involves strong positive words used | |
| # ironically ("Amazing", "Perfect", "Great" all score β₯ 2.0 | |
| # in VADER). If no such word exists, dampen. | |
| if context and not has_hyperbole: | |
| words = [w.strip('.,!?;:"\'-β¦') for w in text_lower.split()] | |
| has_strong_positive = any( | |
| self.sia.lexicon.get(w, 0) >= 2.0 for w in words | |
| ) | |
| if not has_strong_positive: | |
| sarcasm_prob *= 0.4 | |
| sarcasm_prob = max(0.01, min(0.99, sarcasm_prob)) | |
| return { | |
| "score": round(sarcasm_prob, 4), | |
| "raw_score": round(raw_score, 4), | |
| "label": self._get_label(sarcasm_prob) | |
| } | |
| def predict_batch(self, texts: list[str]) -> list[dict]: | |
| """Predict sarcasm for multiple texts.""" | |
| return [self.predict(text) for text in texts] | |
| def _get_label(score: float) -> str: | |
| if score > 0.55: | |
| return "sarcastic" | |
| elif score < 0.35: | |
| return "genuine" | |
| return "uncertain" | |