sarcasm-detector-app / api /predict.py
huggingsam32's picture
Initial commit: Sarcasm Detector WebApp with RoBERTa and Heuristics
798ed0e
Raw
History Blame Contribute Delete
7.08 kB
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]
@staticmethod
def _get_label(score: float) -> str:
if score > 0.55:
return "sarcastic"
elif score < 0.35:
return "genuine"
return "uncertain"