Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -11,7 +11,7 @@ if not os.path.exists("/.dockerenv") and not os.path.exists("/kaggle"):
|
|
| 11 |
import gradio
|
| 12 |
except ImportError:
|
| 13 |
print("Installing required packages...")
|
| 14 |
-
subprocess.check_call([sys.executable, "-m", "pip", "install",
|
| 15 |
"spacy", "matplotlib", "gradio"])
|
| 16 |
# Download spaCy model
|
| 17 |
subprocess.check_call([sys.executable, "-m", "spacy", "download", "en_core_web_md"])
|
|
@@ -32,37 +32,37 @@ nlp = spacy.load("en_core_web_md")
|
|
| 32 |
# Enhanced emotion categories with carefully selected keywords
|
| 33 |
EMOTION_CATEGORIES = {
|
| 34 |
'joy': [
|
| 35 |
-
'happy', 'joyful', 'delighted', 'excited', 'cheerful',
|
| 36 |
'glad', 'elated', 'jubilant', 'overjoyed', 'pleased',
|
| 37 |
'ecstatic', 'thrilled', 'euphoric', 'content', 'blissful'
|
| 38 |
],
|
| 39 |
'sadness': [
|
| 40 |
-
'sad', 'unhappy', 'depressed', 'disappointed', 'sorrowful',
|
| 41 |
'heartbroken', 'melancholy', 'grief', 'somber', 'mournful',
|
| 42 |
'gloomy', 'despondent', 'downcast', 'miserable', 'devastated'
|
| 43 |
],
|
| 44 |
'anger': [
|
| 45 |
-
'angry', 'furious', 'enraged', 'irritated', 'annoyed',
|
| 46 |
'outraged', 'hostile', 'mad', 'infuriated', 'indignant',
|
| 47 |
'livid', 'irate', 'fuming', 'seething', 'resentful'
|
| 48 |
],
|
| 49 |
'fear': [
|
| 50 |
-
'afraid', 'scared', 'frightened', 'terrified', 'anxious',
|
| 51 |
'worried', 'nervous', 'panicked', 'horrified', 'apprehensive',
|
| 52 |
'fearful', 'uneasy', 'alarmed', 'dread', 'paranoid'
|
| 53 |
],
|
| 54 |
'surprise': [
|
| 55 |
-
'surprised', 'amazed', 'astonished', 'shocked', 'stunned',
|
| 56 |
'startled', 'astounded', 'bewildered', 'unexpected', 'awestruck',
|
| 57 |
'flabbergasted', 'dumbfounded', 'incredulous', 'perplexed', 'thunderstruck'
|
| 58 |
],
|
| 59 |
'love': [
|
| 60 |
-
'loving', 'affectionate', 'fond', 'adoring', 'caring',
|
| 61 |
'devoted', 'passionate', 'tender', 'compassionate', 'cherishing',
|
| 62 |
'enamored', 'smitten', 'infatuated', 'admiring', 'doting'
|
| 63 |
],
|
| 64 |
'sarcasm': [
|
| 65 |
-
'sarcastic', 'ironic', 'mocking', 'cynical', 'satirical',
|
| 66 |
'sardonic', 'facetious', 'contemptuous', 'caustic', 'biting',
|
| 67 |
'scornful', 'derisive', 'snide', 'taunting', 'wry'
|
| 68 |
],
|
|
@@ -100,51 +100,51 @@ EMOTION_COLORS = {
|
|
| 100 |
# Common sentiment phrases and expressions for improved detection
|
| 101 |
EMOTION_PHRASES = {
|
| 102 |
'joy': [
|
| 103 |
-
'over the moon', 'on cloud nine', 'couldn\'t be happier',
|
| 104 |
'best day ever', 'made my day', 'feeling great',
|
| 105 |
'absolutely thrilled', 'jumping for joy', 'bursting with happiness',
|
| 106 |
'walking on sunshine', 'flying high', 'tickled pink'
|
| 107 |
],
|
| 108 |
'sadness': [
|
| 109 |
-
'broke my heart', 'in tears', 'feel like crying',
|
| 110 |
'deeply saddened', 'lost all hope', 'feel empty',
|
| 111 |
'devastating news', 'hit hard', 'feel down', 'soul-crushing',
|
| 112 |
'falling apart', 'world is ending', 'deeply hurt'
|
| 113 |
],
|
| 114 |
'anger': [
|
| 115 |
-
'makes my blood boil', 'fed up with', 'had it with',
|
| 116 |
'sick and tired of', 'drives me crazy', 'lost my temper',
|
| 117 |
'absolutely furious', 'beyond frustrated', 'driving me up the wall',
|
| 118 |
'at my wit\'s end', 'through the roof', 'blow a gasket', 'see red'
|
| 119 |
],
|
| 120 |
'fear': [
|
| 121 |
-
'scared to death', 'freaking out', 'keeps me up at night',
|
| 122 |
'terrified of', 'living in fear', 'panic attack',
|
| 123 |
'nervous wreck', 'can\'t stop worrying', 'break out in a cold sweat',
|
| 124 |
'shaking like a leaf', 'scared stiff', 'frozen with fear'
|
| 125 |
],
|
| 126 |
'surprise': [
|
| 127 |
-
'can\'t believe', 'took me by surprise', 'out of nowhere',
|
| 128 |
'never expected', 'caught off guard', 'mind blown',
|
| 129 |
'plot twist', 'jaw dropped', 'knocked my socks off',
|
| 130 |
'took my breath away', 'blew me away', 'speechless'
|
| 131 |
],
|
| 132 |
'love': [
|
| 133 |
-
'deeply in love', 'means the world to me', 'treasure every moment',
|
| 134 |
'hold dear', 'close to my heart', 'forever grateful',
|
| 135 |
'truly blessed', 'never felt this way', 'head over heels',
|
| 136 |
'madly in love', 'heart skips a beat', 'love with all my heart'
|
| 137 |
],
|
| 138 |
'sarcasm': [
|
| 139 |
-
'just what I needed', 'couldn\'t get any better', 'how wonderful',
|
| 140 |
'oh great', 'lucky me', 'my favorite part',
|
| 141 |
'thrilled to bits', 'way to go', 'thanks for nothing',
|
| 142 |
'brilliant job', 'story of my life', 'what a surprise'
|
| 143 |
],
|
| 144 |
'disgust': [
|
| 145 |
-
'makes me sick', 'turn my stomach', 'can\'t stand',
|
| 146 |
'absolutely disgusting', 'utterly repulsive', 'gross',
|
| 147 |
-
'revolting sight', 'nauseating', 'skin crawl',
|
| 148 |
'makes me want to vomit', 'repulsed by', 'can hardly look at'
|
| 149 |
],
|
| 150 |
'anticipation': [
|
|
@@ -170,30 +170,134 @@ CONTEXTUAL_INDICATORS = {
|
|
| 170 |
'punctuation': {'!': 'emphasis', '?': 'question', '...': 'hesitation'}
|
| 171 |
}
|
| 172 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
# Sarcasm patterns with refined detection logic
|
| 174 |
SARCASM_PATTERNS = [
|
| 175 |
# Exaggerated positive with negative context
|
| 176 |
r'(?i)\b(?:so+|really|absolutely|totally|completely)\s+(?:thrilled|excited|happy|delighted)\s+(?:about|with|by)\b.*?(?:terrible|awful|worst|bad)',
|
| 177 |
-
|
| 178 |
# Classic sarcastic phrases
|
| 179 |
r'(?i)(?:^|\W)just\s+what\s+(?:I|we)\s+(?:need|wanted|hoped for)\b',
|
| 180 |
r'(?i)(?:^|\W)how\s+(?:wonderful|nice|great|lovely|exciting)\b.*?(?:\!|\?{2,})',
|
| 181 |
-
|
| 182 |
# Thanks for nothing pattern
|
| 183 |
r'(?i)(?:^|\W)thanks\s+for\s+(?:nothing|that|pointing|stating)\b',
|
| 184 |
-
|
| 185 |
# Quotation marks around positive words (scare quotes)
|
| 186 |
r'(?i)"(?:great|wonderful|excellent|perfect|amazing)"',
|
| 187 |
-
|
| 188 |
# Typical sarcastic responses
|
| 189 |
r'(?i)^(?:yeah|sure|right|oh)\s+(?:right|sure|okay|ok)(?:\W|$)',
|
| 190 |
-
|
| 191 |
# Exaggerated praise in negative context
|
| 192 |
r'(?i)\b(?:brilliant|genius|impressive)\b.*?(?:disaster|failure|mess)',
|
| 193 |
-
|
| 194 |
# Obvious understatements
|
| 195 |
r'(?i)\b(?:slightly|bit|little)\s+(?:catastrophic|disastrous|terrible|awful)\b',
|
| 196 |
-
|
| 197 |
# Oh great patterns
|
| 198 |
r'(?i)(?:^|\W)oh\s+(?:great|wonderful|perfect|fantastic|awesome)(?:\W|$)'
|
| 199 |
]
|
|
@@ -208,16 +312,16 @@ def detect_phrases(text, emotion_phrases):
|
|
| 208 |
"""Detect emotion-specific phrases in text"""
|
| 209 |
text_lower = text.lower()
|
| 210 |
detected_phrases = {}
|
| 211 |
-
|
| 212 |
for emotion, phrases in emotion_phrases.items():
|
| 213 |
found_phrases = []
|
| 214 |
for phrase in phrases:
|
| 215 |
if phrase.lower() in text_lower:
|
| 216 |
found_phrases.append(phrase)
|
| 217 |
-
|
| 218 |
if found_phrases:
|
| 219 |
detected_phrases[emotion] = found_phrases
|
| 220 |
-
|
| 221 |
return detected_phrases
|
| 222 |
|
| 223 |
def detect_contextual_features(text):
|
|
@@ -232,12 +336,12 @@ def detect_contextual_features(text):
|
|
| 232 |
'ellipses': text.count('...'),
|
| 233 |
'capitalized_words': len(re.findall(r'\b[A-Z]{2,}\b', text))
|
| 234 |
}
|
| 235 |
-
|
| 236 |
doc = nlp(text.lower())
|
| 237 |
-
|
| 238 |
# Get tokens for counting
|
| 239 |
tokens = [token.text for token in doc]
|
| 240 |
-
|
| 241 |
# Count contextual indicators
|
| 242 |
for indicator_type, words in CONTEXTUAL_INDICATORS.items():
|
| 243 |
if indicator_type != 'punctuation':
|
|
@@ -247,7 +351,7 @@ def detect_contextual_features(text):
|
|
| 247 |
features[indicator_type] += 1
|
| 248 |
else: # Single word
|
| 249 |
features[indicator_type] += tokens.count(word)
|
| 250 |
-
|
| 251 |
return features
|
| 252 |
|
| 253 |
def detect_sarcasm_patterns(text):
|
|
@@ -255,42 +359,42 @@ def detect_sarcasm_patterns(text):
|
|
| 255 |
# Match sarcasm patterns
|
| 256 |
matches = 0
|
| 257 |
pattern_matches = []
|
| 258 |
-
|
| 259 |
for pattern in SARCASM_PATTERNS:
|
| 260 |
if re.search(pattern, text):
|
| 261 |
matches += 1
|
| 262 |
pattern_matches.append(pattern)
|
| 263 |
-
|
| 264 |
# Get contextual features
|
| 265 |
features = detect_contextual_features(text)
|
| 266 |
-
|
| 267 |
# Check for phrases specific to sarcasm
|
| 268 |
phrases = detect_phrases(text, {'sarcasm': EMOTION_PHRASES['sarcasm']})
|
| 269 |
sarcasm_phrases = len(phrases.get('sarcasm', []))
|
| 270 |
-
|
| 271 |
# Calculate raw score based on pattern matches and features
|
| 272 |
raw_score = (matches * 0.15) + (sarcasm_phrases * 0.2)
|
| 273 |
-
|
| 274 |
# Adjust based on contextual features
|
| 275 |
if features['exclamations'] > 1:
|
| 276 |
raw_score += min(features['exclamations'] * 0.05, 0.2)
|
| 277 |
-
|
| 278 |
if features['capitalized_words'] > 0:
|
| 279 |
raw_score += min(features['capitalized_words'] * 0.1, 0.3)
|
| 280 |
-
|
| 281 |
# Detect positive-negative contrasts
|
| 282 |
pos_neg_contrast = 0
|
| 283 |
emotion_phrases = detect_phrases(text, {
|
| 284 |
'positive': EMOTION_PHRASES['joy'] + EMOTION_PHRASES['love'],
|
| 285 |
'negative': EMOTION_PHRASES['sadness'] + EMOTION_PHRASES['anger']
|
| 286 |
})
|
| 287 |
-
|
| 288 |
if emotion_phrases.get('positive') and emotion_phrases.get('negative'):
|
| 289 |
pos_neg_contrast = 0.3
|
| 290 |
-
|
| 291 |
# Add contrast score
|
| 292 |
raw_score += pos_neg_contrast
|
| 293 |
-
|
| 294 |
# Normalize to [0, 1]
|
| 295 |
return min(raw_score, 1.0), pattern_matches
|
| 296 |
|
|
@@ -298,13 +402,13 @@ def calculate_emotion_similarity(text, emotion_keywords):
|
|
| 298 |
"""Calculate similarity between text and emotion keywords using spaCy"""
|
| 299 |
if not text.strip():
|
| 300 |
return 0.0
|
| 301 |
-
|
| 302 |
# Process the input text
|
| 303 |
doc = nlp(text.lower())
|
| 304 |
-
|
| 305 |
# Get average similarity with emotion keywords
|
| 306 |
keyword_scores = []
|
| 307 |
-
|
| 308 |
# Use a subset of keywords for efficiency
|
| 309 |
for keyword in emotion_keywords[:6]: # Use first 6 keywords for each emotion
|
| 310 |
keyword_doc = nlp(keyword)
|
|
@@ -315,9 +419,9 @@ def calculate_emotion_similarity(text, emotion_keywords):
|
|
| 315 |
for keyword_token in keyword_doc:
|
| 316 |
similarity = token.similarity(keyword_token)
|
| 317 |
max_similarity = max(max_similarity, similarity)
|
| 318 |
-
|
| 319 |
keyword_scores.append(max_similarity)
|
| 320 |
-
|
| 321 |
# Return average of top 3 similarities if we have at least 3 scores
|
| 322 |
if len(keyword_scores) >= 3:
|
| 323 |
return sum(sorted(keyword_scores, reverse=True)[:3]) / 3
|
|
@@ -331,40 +435,40 @@ def get_emotion_score(text, emotion, keywords):
|
|
| 331 |
"""Calculate emotion score based on similarity, context, and phrase detection"""
|
| 332 |
# Get emotion score using spaCy word vectors
|
| 333 |
similarity_score = calculate_emotion_similarity(text, keywords)
|
| 334 |
-
|
| 335 |
# Check for emotion-specific phrases
|
| 336 |
detected_phrases = detect_phrases(text, {emotion: EMOTION_PHRASES[emotion]})
|
| 337 |
phrase_count = len(detected_phrases.get(emotion, []))
|
| 338 |
phrase_score = min(phrase_count * 0.2, 0.6) # Cap at 0.6
|
| 339 |
-
|
| 340 |
# Get contextual features
|
| 341 |
features = detect_contextual_features(text)
|
| 342 |
-
|
| 343 |
# Calculate feature-based adjustment
|
| 344 |
feature_adjustment = 0
|
| 345 |
-
|
| 346 |
# Search for direct emotion mentions in text
|
| 347 |
doc = nlp(text.lower())
|
| 348 |
direct_mention_score = 0
|
| 349 |
-
|
| 350 |
for token in doc:
|
| 351 |
if token.lemma_ in keywords:
|
| 352 |
direct_mention_score += 0.2 # Direct mention of emotion word
|
| 353 |
break
|
| 354 |
-
|
| 355 |
# Adjust score based on emotional context
|
| 356 |
if emotion in ['joy', 'love', 'surprise'] and features['exclamations'] > 0:
|
| 357 |
feature_adjustment += min(features['exclamations'] * 0.05, 0.2)
|
| 358 |
-
|
| 359 |
if emotion in ['anger', 'sadness'] and features['negators'] > 0:
|
| 360 |
feature_adjustment += min(features['negators'] * 0.05, 0.2)
|
| 361 |
-
|
| 362 |
if emotion == 'fear' and features['intensifiers'] > 0:
|
| 363 |
feature_adjustment += min(features['intensifiers'] * 0.05, 0.2)
|
| 364 |
-
|
| 365 |
# Combine scores with appropriate weights
|
| 366 |
final_score = (similarity_score * 0.5) + (phrase_score * 0.3) + (feature_adjustment * 0.1) + (direct_mention_score * 0.1)
|
| 367 |
-
|
| 368 |
# Normalize to ensure it's in [0, 1]
|
| 369 |
return max(0, min(final_score, 1.0)), detected_phrases.get(emotion, [])
|
| 370 |
|
|
@@ -373,37 +477,37 @@ def analyze_sarcasm(text):
|
|
| 373 |
# 1. Keyword similarity for sarcasm words
|
| 374 |
sarcasm_keywords = EMOTION_CATEGORIES['sarcasm']
|
| 375 |
similarity_score = calculate_emotion_similarity(text, sarcasm_keywords)
|
| 376 |
-
|
| 377 |
# 2. Linguistic pattern detection
|
| 378 |
pattern_score, pattern_matches = detect_sarcasm_patterns(text)
|
| 379 |
-
|
| 380 |
# 3. Check for semantic incongruity between sentences
|
| 381 |
incongruity_score = 0
|
| 382 |
sentences = list(nlp(text).sents)
|
| 383 |
-
|
| 384 |
if len(sentences) > 1:
|
| 385 |
# Calculate similarity between adjacent sentences
|
| 386 |
similarities = []
|
| 387 |
for i in range(len(sentences) - 1):
|
| 388 |
sim = sentences[i].similarity(sentences[i+1])
|
| 389 |
similarities.append(sim)
|
| 390 |
-
|
| 391 |
# Low similarity between adjacent sentences might indicate sarcasm
|
| 392 |
if similarities and min(similarities) < 0.5:
|
| 393 |
incongruity_score = 0.3
|
| 394 |
-
|
| 395 |
# 4. Check for sarcasm phrases
|
| 396 |
detected_phrases = detect_phrases(text, {'sarcasm': EMOTION_PHRASES['sarcasm']})
|
| 397 |
phrase_score = min(len(detected_phrases.get('sarcasm', [])) * 0.2, 0.6)
|
| 398 |
-
|
| 399 |
# 5. Check for emotional contrast
|
| 400 |
# (positive words in negative context or vice versa)
|
| 401 |
doc = nlp(text.lower())
|
| 402 |
-
|
| 403 |
# Count positive and negative words
|
| 404 |
pos_count = 0
|
| 405 |
neg_count = 0
|
| 406 |
-
|
| 407 |
for token in doc:
|
| 408 |
if token.is_alpha and not token.is_stop:
|
| 409 |
# Check against positive and negative emotion keywords
|
|
@@ -411,40 +515,98 @@ def analyze_sarcasm(text):
|
|
| 411 |
pos_count += 1
|
| 412 |
if any(token.similarity(nlp(word)) > 0.7 for word in EMOTION_CATEGORIES['sadness'][:5] + EMOTION_CATEGORIES['anger'][:5]):
|
| 413 |
neg_count += 1
|
| 414 |
-
|
| 415 |
contrast_score = 0
|
| 416 |
if pos_count > 0 and neg_count > 0:
|
| 417 |
contrast_score = min(0.3, pos_count * neg_count * 0.05)
|
| 418 |
-
|
| 419 |
# Weighted combination of all scores
|
| 420 |
combined_score = (0.15 * similarity_score) + (0.35 * pattern_score) + \
|
| 421 |
(0.15 * incongruity_score) + (0.25 * phrase_score) + \
|
| 422 |
(0.1 * contrast_score)
|
| 423 |
-
|
| 424 |
# Normalize to [0, 1]
|
| 425 |
return max(0, min(combined_score, 1.0)), detected_phrases.get('sarcasm', []), pattern_matches
|
| 426 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
def analyze_emotions(text):
|
| 428 |
-
"""Analyze emotions in text using spaCy with robust sarcasm detection"""
|
| 429 |
if not text or not text.strip():
|
| 430 |
return None, {"error": "Please enter some text to analyze"}
|
| 431 |
-
|
| 432 |
try:
|
| 433 |
# Calculate scores for each emotion with supporting phrases
|
| 434 |
emotion_data = {}
|
| 435 |
-
|
| 436 |
# For each standard emotion category (excluding sarcasm)
|
| 437 |
for emotion, keywords in EMOTION_CATEGORIES.items():
|
| 438 |
if emotion == 'sarcasm':
|
| 439 |
continue
|
| 440 |
-
|
| 441 |
# Use specialized function to get emotion score and supporting phrases
|
| 442 |
score, phrases = get_emotion_score(text, emotion, keywords)
|
| 443 |
emotion_data[emotion] = {
|
| 444 |
'score': score,
|
| 445 |
'phrases': phrases
|
| 446 |
}
|
| 447 |
-
|
| 448 |
# Special handling for sarcasm with multi-method approach
|
| 449 |
sarcasm_score, sarcasm_phrases, sarcasm_patterns = analyze_sarcasm(text)
|
| 450 |
emotion_data['sarcasm'] = {
|
|
@@ -452,182 +614,166 @@ def analyze_emotions(text):
|
|
| 452 |
'phrases': sarcasm_phrases,
|
| 453 |
'patterns': sarcasm_patterns
|
| 454 |
}
|
| 455 |
-
|
| 456 |
# Get contextual features for overall analysis
|
| 457 |
context_features = detect_contextual_features(text)
|
| 458 |
-
|
| 459 |
# Apply decision making for final analysis
|
| 460 |
# 1. Check for dominant emotions by raw scores
|
| 461 |
emotion_scores = {emotion: data['score'] for emotion, data in emotion_data.items()}
|
| 462 |
-
|
| 463 |
# 2. Adjust based on contextual evidence
|
| 464 |
-
# If we have strong phrase evidence, boost
|
| 465 |
for emotion, data in emotion_data.items():
|
| 466 |
-
if len(data.get('phrases', []))
|
| 467 |
-
emotion_scores[emotion] = emotion_scores[emotion] * 1.2
|
| 468 |
-
|
| 469 |
-
# 3.
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
total_score = sum(emotion_scores.values())
|
| 492 |
-
normalized_scores = {emotion: score / total_score for emotion, score in emotion_scores.items()}
|
| 493 |
-
|
| 494 |
-
# Sort emotions by score
|
| 495 |
-
sorted_emotions = sorted(normalized_scores.items(), key=lambda x: x[1], reverse=True)
|
| 496 |
-
emotions, scores = zip(*sorted_emotions)
|
| 497 |
-
|
| 498 |
-
# Prepare supporting evidence for each emotion
|
| 499 |
-
supporting_evidence = {}
|
| 500 |
-
for emotion in emotions:
|
| 501 |
-
evidence = []
|
| 502 |
-
|
| 503 |
-
# Add detected phrases
|
| 504 |
-
if emotion_data[emotion].get('phrases'):
|
| 505 |
-
evidence.extend([f'Phrase: "{phrase}"' for phrase in emotion_data[emotion]['phrases']])
|
| 506 |
-
|
| 507 |
-
# Add pattern matches for sarcasm
|
| 508 |
-
if emotion == 'sarcasm' and emotion_data['sarcasm'].get('patterns'):
|
| 509 |
-
evidence.extend([f'Pattern match: sarcastic pattern detected' for _ in emotion_data['sarcasm']['patterns']])
|
| 510 |
-
|
| 511 |
-
# Add contextual features as evidence
|
| 512 |
-
if emotion == 'joy' and context_features['exclamations'] > 1:
|
| 513 |
-
evidence.append(f'Found {context_features["exclamations"]} exclamation marks (!)')
|
| 514 |
-
|
| 515 |
-
if emotion == 'anger' and context_features['capitalized_words'] > 0:
|
| 516 |
-
evidence.append(f'Found {context_features["capitalized_words"]} capitalized words')
|
| 517 |
-
|
| 518 |
-
supporting_evidence[emotion] = evidence[:3] # Limit to top 3 pieces of evidence
|
| 519 |
-
|
| 520 |
-
# Create visualization
|
| 521 |
-
fig = create_visualization(emotions, scores, text, supporting_evidence)
|
| 522 |
-
|
| 523 |
-
# Format output
|
| 524 |
-
output = {
|
| 525 |
-
"dominant_emotion": emotions[0],
|
| 526 |
-
"confidence": f"{scores[0]*100:.1f}%",
|
| 527 |
-
"detailed_scores": {emotion: f"{score*100:.1f}%" for emotion, score in zip(emotions, scores)},
|
| 528 |
-
"supporting_evidence": supporting_evidence
|
| 529 |
}
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
output["note"] = f"Sarcasm detected with {scores[0]*100:.1f}% confidence."
|
| 534 |
-
elif 'sarcasm' in normalized_scores and normalized_scores['sarcasm'] > 0.25:
|
| 535 |
-
output["note"] = f"Some sarcastic elements detected alongside {emotions[0]}."
|
| 536 |
-
|
| 537 |
-
return fig, output
|
| 538 |
-
|
| 539 |
except Exception as e:
|
| 540 |
import traceback
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
fig, ax = plt.subplots(figsize=(
|
| 548 |
-
|
| 549 |
-
#
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
#
|
| 557 |
-
ax.
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
ax.
|
| 563 |
-
|
| 564 |
-
#
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
ax.set_yticklabels(y_labels)
|
| 581 |
-
ax.invert_yaxis() # Labels read top-to-bottom
|
| 582 |
-
|
| 583 |
-
# Add value labels to the bars
|
| 584 |
-
for i, v in enumerate(scores):
|
| 585 |
-
ax.text(v * 100 + 1, i, f"{v*100:.1f}%", va='center')
|
| 586 |
-
|
| 587 |
-
# Set title with truncated text if provided
|
| 588 |
-
if text:
|
| 589 |
-
display_text = text if len(text) < 50 else text[:47] + "..."
|
| 590 |
-
ax.set_title(f'Emotion Analysis: "{display_text}"', pad=20)
|
| 591 |
-
else:
|
| 592 |
-
ax.set_title('spaCy-based Emotion Analysis', pad=20)
|
| 593 |
-
|
| 594 |
plt.tight_layout()
|
|
|
|
|
|
|
|
|
|
| 595 |
return fig
|
| 596 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 597 |
# Create Gradio interface
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 628 |
|
| 629 |
-
# Launch the
|
| 630 |
if __name__ == "__main__":
|
| 631 |
-
print("
|
| 632 |
-
|
| 633 |
-
demo.launch(
|
|
|
|
|
|
| 11 |
import gradio
|
| 12 |
except ImportError:
|
| 13 |
print("Installing required packages...")
|
| 14 |
+
subprocess.check_call([sys.executable, "-m", "pip", "install",
|
| 15 |
"spacy", "matplotlib", "gradio"])
|
| 16 |
# Download spaCy model
|
| 17 |
subprocess.check_call([sys.executable, "-m", "spacy", "download", "en_core_web_md"])
|
|
|
|
| 32 |
# Enhanced emotion categories with carefully selected keywords
|
| 33 |
EMOTION_CATEGORIES = {
|
| 34 |
'joy': [
|
| 35 |
+
'happy', 'joyful', 'delighted', 'excited', 'cheerful',
|
| 36 |
'glad', 'elated', 'jubilant', 'overjoyed', 'pleased',
|
| 37 |
'ecstatic', 'thrilled', 'euphoric', 'content', 'blissful'
|
| 38 |
],
|
| 39 |
'sadness': [
|
| 40 |
+
'sad', 'unhappy', 'depressed', 'disappointed', 'sorrowful',
|
| 41 |
'heartbroken', 'melancholy', 'grief', 'somber', 'mournful',
|
| 42 |
'gloomy', 'despondent', 'downcast', 'miserable', 'devastated'
|
| 43 |
],
|
| 44 |
'anger': [
|
| 45 |
+
'angry', 'furious', 'enraged', 'irritated', 'annoyed',
|
| 46 |
'outraged', 'hostile', 'mad', 'infuriated', 'indignant',
|
| 47 |
'livid', 'irate', 'fuming', 'seething', 'resentful'
|
| 48 |
],
|
| 49 |
'fear': [
|
| 50 |
+
'afraid', 'scared', 'frightened', 'terrified', 'anxious',
|
| 51 |
'worried', 'nervous', 'panicked', 'horrified', 'apprehensive',
|
| 52 |
'fearful', 'uneasy', 'alarmed', 'dread', 'paranoid'
|
| 53 |
],
|
| 54 |
'surprise': [
|
| 55 |
+
'surprised', 'amazed', 'astonished', 'shocked', 'stunned',
|
| 56 |
'startled', 'astounded', 'bewildered', 'unexpected', 'awestruck',
|
| 57 |
'flabbergasted', 'dumbfounded', 'incredulous', 'perplexed', 'thunderstruck'
|
| 58 |
],
|
| 59 |
'love': [
|
| 60 |
+
'loving', 'affectionate', 'fond', 'adoring', 'caring',
|
| 61 |
'devoted', 'passionate', 'tender', 'compassionate', 'cherishing',
|
| 62 |
'enamored', 'smitten', 'infatuated', 'admiring', 'doting'
|
| 63 |
],
|
| 64 |
'sarcasm': [
|
| 65 |
+
'sarcastic', 'ironic', 'mocking', 'cynical', 'satirical',
|
| 66 |
'sardonic', 'facetious', 'contemptuous', 'caustic', 'biting',
|
| 67 |
'scornful', 'derisive', 'snide', 'taunting', 'wry'
|
| 68 |
],
|
|
|
|
| 100 |
# Common sentiment phrases and expressions for improved detection
|
| 101 |
EMOTION_PHRASES = {
|
| 102 |
'joy': [
|
| 103 |
+
'over the moon', 'on cloud nine', 'couldn\'t be happier',
|
| 104 |
'best day ever', 'made my day', 'feeling great',
|
| 105 |
'absolutely thrilled', 'jumping for joy', 'bursting with happiness',
|
| 106 |
'walking on sunshine', 'flying high', 'tickled pink'
|
| 107 |
],
|
| 108 |
'sadness': [
|
| 109 |
+
'broke my heart', 'in tears', 'feel like crying',
|
| 110 |
'deeply saddened', 'lost all hope', 'feel empty',
|
| 111 |
'devastating news', 'hit hard', 'feel down', 'soul-crushing',
|
| 112 |
'falling apart', 'world is ending', 'deeply hurt'
|
| 113 |
],
|
| 114 |
'anger': [
|
| 115 |
+
'makes my blood boil', 'fed up with', 'had it with',
|
| 116 |
'sick and tired of', 'drives me crazy', 'lost my temper',
|
| 117 |
'absolutely furious', 'beyond frustrated', 'driving me up the wall',
|
| 118 |
'at my wit\'s end', 'through the roof', 'blow a gasket', 'see red'
|
| 119 |
],
|
| 120 |
'fear': [
|
| 121 |
+
'scared to death', 'freaking out', 'keeps me up at night',
|
| 122 |
'terrified of', 'living in fear', 'panic attack',
|
| 123 |
'nervous wreck', 'can\'t stop worrying', 'break out in a cold sweat',
|
| 124 |
'shaking like a leaf', 'scared stiff', 'frozen with fear'
|
| 125 |
],
|
| 126 |
'surprise': [
|
| 127 |
+
'can\'t believe', 'took me by surprise', 'out of nowhere',
|
| 128 |
'never expected', 'caught off guard', 'mind blown',
|
| 129 |
'plot twist', 'jaw dropped', 'knocked my socks off',
|
| 130 |
'took my breath away', 'blew me away', 'speechless'
|
| 131 |
],
|
| 132 |
'love': [
|
| 133 |
+
'deeply in love', 'means the world to me', 'treasure every moment',
|
| 134 |
'hold dear', 'close to my heart', 'forever grateful',
|
| 135 |
'truly blessed', 'never felt this way', 'head over heels',
|
| 136 |
'madly in love', 'heart skips a beat', 'love with all my heart'
|
| 137 |
],
|
| 138 |
'sarcasm': [
|
| 139 |
+
'just what I needed', 'couldn\'t get any better', 'how wonderful',
|
| 140 |
'oh great', 'lucky me', 'my favorite part',
|
| 141 |
'thrilled to bits', 'way to go', 'thanks for nothing',
|
| 142 |
'brilliant job', 'story of my life', 'what a surprise'
|
| 143 |
],
|
| 144 |
'disgust': [
|
| 145 |
+
'makes me sick', 'turn my stomach', 'can\'t stand',
|
| 146 |
'absolutely disgusting', 'utterly repulsive', 'gross',
|
| 147 |
+
'revolting sight', 'nauseating', 'skin crawl',
|
| 148 |
'makes me want to vomit', 'repulsed by', 'can hardly look at'
|
| 149 |
],
|
| 150 |
'anticipation': [
|
|
|
|
| 170 |
'punctuation': {'!': 'emphasis', '?': 'question', '...': 'hesitation'}
|
| 171 |
}
|
| 172 |
|
| 173 |
+
# Emotional verdict categories for intelligently classifying mixed emotions
|
| 174 |
+
EMOTION_VERDICT_CATEGORIES = {
|
| 175 |
+
# Single dominant emotions (when over 35%)
|
| 176 |
+
'purely_joyful': {'conditions': [('joy', 0.35)], 'label': 'Purely Joyful', 'description': 'Expressing happiness and positive emotions'},
|
| 177 |
+
'deeply_sad': {'conditions': [('sadness', 0.35)], 'label': 'Deeply Sad', 'description': 'Expressing sadness and negative emotions'},
|
| 178 |
+
'intensely_angry': {'conditions': [('anger', 0.35)], 'label': 'Intensely Angry', 'description': 'Expressing anger and frustration'},
|
| 179 |
+
'primarily_fearful': {'conditions': [('fear', 0.35)], 'label': 'Primarily Fearful', 'description': 'Expressing fear and anxiety'},
|
| 180 |
+
'genuinely_surprised': {'conditions': [('surprise', 0.35)], 'label': 'Genuinely Surprised', 'description': 'Expressing surprise and astonishment'},
|
| 181 |
+
'deeply_loving': {'conditions': [('love', 0.35)], 'label': 'Deeply Loving', 'description': 'Expressing love and affection'},
|
| 182 |
+
'clearly_sarcastic': {'conditions': [('sarcasm', 0.35)], 'label': 'Clearly Sarcastic', 'description': 'Expressing sarcasm and irony'},
|
| 183 |
+
'utterly_disgusted': {'conditions': [('disgust', 0.35)], 'label': 'Utterly Disgusted', 'description': 'Expressing disgust and revulsion'},
|
| 184 |
+
'eagerly_anticipating': {'conditions': [('anticipation', 0.35)], 'label': 'Eagerly Anticipating', 'description': 'Expressing anticipation and eagerness'},
|
| 185 |
+
'firmly_trusting': {'conditions': [('trust', 0.35)], 'label': 'Firmly Trusting', 'description': 'Expressing trust and confidence'},
|
| 186 |
+
|
| 187 |
+
# Common emotional combinations
|
| 188 |
+
'bitter_sweet': {
|
| 189 |
+
'conditions': [('joy', 0.2), ('sadness', 0.2)],
|
| 190 |
+
'label': 'Bittersweet',
|
| 191 |
+
'description': 'Mixed feelings of happiness and sadness'
|
| 192 |
+
},
|
| 193 |
+
'anxious_excitement': {
|
| 194 |
+
'conditions': [('anticipation', 0.2), ('fear', 0.2)],
|
| 195 |
+
'label': 'Anxious Excitement',
|
| 196 |
+
'description': 'Mixture of excitement and nervousness'
|
| 197 |
+
},
|
| 198 |
+
'angry_disappointment': {
|
| 199 |
+
'conditions': [('anger', 0.2), ('sadness', 0.2)],
|
| 200 |
+
'label': 'Angry Disappointment',
|
| 201 |
+
'description': 'Disappointment expressed through anger'
|
| 202 |
+
},
|
| 203 |
+
'ironic_amusement': {
|
| 204 |
+
'conditions': [('sarcasm', 0.2), ('joy', 0.15)],
|
| 205 |
+
'label': 'Ironic Amusement',
|
| 206 |
+
'description': 'Finding humor through irony or sarcasm'
|
| 207 |
+
},
|
| 208 |
+
'fearful_anticipation': {
|
| 209 |
+
'conditions': [('fear', 0.2), ('anticipation', 0.2)],
|
| 210 |
+
'label': 'Fearful Anticipation',
|
| 211 |
+
'description': 'Anxiously awaiting something'
|
| 212 |
+
},
|
| 213 |
+
'relieved_surprise': {
|
| 214 |
+
'conditions': [('surprise', 0.2), ('joy', 0.15)],
|
| 215 |
+
'label': 'Relieved Surprise',
|
| 216 |
+
'description': 'Surprise with positive outcome'
|
| 217 |
+
},
|
| 218 |
+
'shocked_disappointment': {
|
| 219 |
+
'conditions': [('surprise', 0.2), ('sadness', 0.15)],
|
| 220 |
+
'label': 'Shocked Disappointment',
|
| 221 |
+
'description': 'Unexpectedly negative outcome'
|
| 222 |
+
},
|
| 223 |
+
'disgusted_anger': {
|
| 224 |
+
'conditions': [('disgust', 0.2), ('anger', 0.2)],
|
| 225 |
+
'label': 'Disgusted Anger',
|
| 226 |
+
'description': 'Angry response to something repulsive'
|
| 227 |
+
},
|
| 228 |
+
'loving_trust': {
|
| 229 |
+
'conditions': [('love', 0.2), ('trust', 0.2)],
|
| 230 |
+
'label': 'Loving Trust',
|
| 231 |
+
'description': 'Deep affection with confidence'
|
| 232 |
+
},
|
| 233 |
+
'sarcastic_frustration': {
|
| 234 |
+
'conditions': [('sarcasm', 0.2), ('anger', 0.15)],
|
| 235 |
+
'label': 'Sarcastic Frustration',
|
| 236 |
+
'description': 'Using sarcasm to express frustration'
|
| 237 |
+
},
|
| 238 |
+
'confused_surprise': {
|
| 239 |
+
'conditions': [('surprise', 0.2), ('fear', 0.15)],
|
| 240 |
+
'label': 'Confused Surprise',
|
| 241 |
+
'description': 'Startled with uncertainty'
|
| 242 |
+
},
|
| 243 |
+
'hopeful_joy': {
|
| 244 |
+
'conditions': [('joy', 0.2), ('anticipation', 0.2)],
|
| 245 |
+
'label': 'Hopeful Joy',
|
| 246 |
+
'description': 'Happy anticipation of something positive'
|
| 247 |
+
},
|
| 248 |
+
'betrayed_trust': {
|
| 249 |
+
'conditions': [('sadness', 0.2), ('trust', 0.15), ('anger', 0.15)],
|
| 250 |
+
'label': 'Betrayed Trust',
|
| 251 |
+
'description': 'Sadness from broken trust'
|
| 252 |
+
},
|
| 253 |
+
'fearful_disgust': {
|
| 254 |
+
'conditions': [('fear', 0.2), ('disgust', 0.2)],
|
| 255 |
+
'label': 'Fearful Disgust',
|
| 256 |
+
'description': 'Fear of something repulsive'
|
| 257 |
+
},
|
| 258 |
+
|
| 259 |
+
# Special cases for multiple emotions
|
| 260 |
+
'emotionally_complex': {
|
| 261 |
+
'conditions': ['multiple_over_15'],
|
| 262 |
+
'label': 'Emotionally Complex',
|
| 263 |
+
'description': 'Multiple competing emotions'
|
| 264 |
+
},
|
| 265 |
+
'mildly_emotional': {
|
| 266 |
+
'conditions': ['all_under_20'],
|
| 267 |
+
'label': 'Mildly Emotional',
|
| 268 |
+
'description': 'Low intensity emotional content'
|
| 269 |
+
},
|
| 270 |
+
'predominantly_neutral': {
|
| 271 |
+
'conditions': ['all_under_15'],
|
| 272 |
+
'label': 'Predominantly Neutral',
|
| 273 |
+
'description': 'No strong emotional signals detected'
|
| 274 |
+
}
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
# Sarcasm patterns with refined detection logic
|
| 278 |
SARCASM_PATTERNS = [
|
| 279 |
# Exaggerated positive with negative context
|
| 280 |
r'(?i)\b(?:so+|really|absolutely|totally|completely)\s+(?:thrilled|excited|happy|delighted)\s+(?:about|with|by)\b.*?(?:terrible|awful|worst|bad)',
|
| 281 |
+
|
| 282 |
# Classic sarcastic phrases
|
| 283 |
r'(?i)(?:^|\W)just\s+what\s+(?:I|we)\s+(?:need|wanted|hoped for)\b',
|
| 284 |
r'(?i)(?:^|\W)how\s+(?:wonderful|nice|great|lovely|exciting)\b.*?(?:\!|\?{2,})',
|
| 285 |
+
|
| 286 |
# Thanks for nothing pattern
|
| 287 |
r'(?i)(?:^|\W)thanks\s+for\s+(?:nothing|that|pointing|stating)\b',
|
| 288 |
+
|
| 289 |
# Quotation marks around positive words (scare quotes)
|
| 290 |
r'(?i)"(?:great|wonderful|excellent|perfect|amazing)"',
|
| 291 |
+
|
| 292 |
# Typical sarcastic responses
|
| 293 |
r'(?i)^(?:yeah|sure|right|oh)\s+(?:right|sure|okay|ok)(?:\W|$)',
|
| 294 |
+
|
| 295 |
# Exaggerated praise in negative context
|
| 296 |
r'(?i)\b(?:brilliant|genius|impressive)\b.*?(?:disaster|failure|mess)',
|
| 297 |
+
|
| 298 |
# Obvious understatements
|
| 299 |
r'(?i)\b(?:slightly|bit|little)\s+(?:catastrophic|disastrous|terrible|awful)\b',
|
| 300 |
+
|
| 301 |
# Oh great patterns
|
| 302 |
r'(?i)(?:^|\W)oh\s+(?:great|wonderful|perfect|fantastic|awesome)(?:\W|$)'
|
| 303 |
]
|
|
|
|
| 312 |
"""Detect emotion-specific phrases in text"""
|
| 313 |
text_lower = text.lower()
|
| 314 |
detected_phrases = {}
|
| 315 |
+
|
| 316 |
for emotion, phrases in emotion_phrases.items():
|
| 317 |
found_phrases = []
|
| 318 |
for phrase in phrases:
|
| 319 |
if phrase.lower() in text_lower:
|
| 320 |
found_phrases.append(phrase)
|
| 321 |
+
|
| 322 |
if found_phrases:
|
| 323 |
detected_phrases[emotion] = found_phrases
|
| 324 |
+
|
| 325 |
return detected_phrases
|
| 326 |
|
| 327 |
def detect_contextual_features(text):
|
|
|
|
| 336 |
'ellipses': text.count('...'),
|
| 337 |
'capitalized_words': len(re.findall(r'\b[A-Z]{2,}\b', text))
|
| 338 |
}
|
| 339 |
+
|
| 340 |
doc = nlp(text.lower())
|
| 341 |
+
|
| 342 |
# Get tokens for counting
|
| 343 |
tokens = [token.text for token in doc]
|
| 344 |
+
|
| 345 |
# Count contextual indicators
|
| 346 |
for indicator_type, words in CONTEXTUAL_INDICATORS.items():
|
| 347 |
if indicator_type != 'punctuation':
|
|
|
|
| 351 |
features[indicator_type] += 1
|
| 352 |
else: # Single word
|
| 353 |
features[indicator_type] += tokens.count(word)
|
| 354 |
+
|
| 355 |
return features
|
| 356 |
|
| 357 |
def detect_sarcasm_patterns(text):
|
|
|
|
| 359 |
# Match sarcasm patterns
|
| 360 |
matches = 0
|
| 361 |
pattern_matches = []
|
| 362 |
+
|
| 363 |
for pattern in SARCASM_PATTERNS:
|
| 364 |
if re.search(pattern, text):
|
| 365 |
matches += 1
|
| 366 |
pattern_matches.append(pattern)
|
| 367 |
+
|
| 368 |
# Get contextual features
|
| 369 |
features = detect_contextual_features(text)
|
| 370 |
+
|
| 371 |
# Check for phrases specific to sarcasm
|
| 372 |
phrases = detect_phrases(text, {'sarcasm': EMOTION_PHRASES['sarcasm']})
|
| 373 |
sarcasm_phrases = len(phrases.get('sarcasm', []))
|
| 374 |
+
|
| 375 |
# Calculate raw score based on pattern matches and features
|
| 376 |
raw_score = (matches * 0.15) + (sarcasm_phrases * 0.2)
|
| 377 |
+
|
| 378 |
# Adjust based on contextual features
|
| 379 |
if features['exclamations'] > 1:
|
| 380 |
raw_score += min(features['exclamations'] * 0.05, 0.2)
|
| 381 |
+
|
| 382 |
if features['capitalized_words'] > 0:
|
| 383 |
raw_score += min(features['capitalized_words'] * 0.1, 0.3)
|
| 384 |
+
|
| 385 |
# Detect positive-negative contrasts
|
| 386 |
pos_neg_contrast = 0
|
| 387 |
emotion_phrases = detect_phrases(text, {
|
| 388 |
'positive': EMOTION_PHRASES['joy'] + EMOTION_PHRASES['love'],
|
| 389 |
'negative': EMOTION_PHRASES['sadness'] + EMOTION_PHRASES['anger']
|
| 390 |
})
|
| 391 |
+
|
| 392 |
if emotion_phrases.get('positive') and emotion_phrases.get('negative'):
|
| 393 |
pos_neg_contrast = 0.3
|
| 394 |
+
|
| 395 |
# Add contrast score
|
| 396 |
raw_score += pos_neg_contrast
|
| 397 |
+
|
| 398 |
# Normalize to [0, 1]
|
| 399 |
return min(raw_score, 1.0), pattern_matches
|
| 400 |
|
|
|
|
| 402 |
"""Calculate similarity between text and emotion keywords using spaCy"""
|
| 403 |
if not text.strip():
|
| 404 |
return 0.0
|
| 405 |
+
|
| 406 |
# Process the input text
|
| 407 |
doc = nlp(text.lower())
|
| 408 |
+
|
| 409 |
# Get average similarity with emotion keywords
|
| 410 |
keyword_scores = []
|
| 411 |
+
|
| 412 |
# Use a subset of keywords for efficiency
|
| 413 |
for keyword in emotion_keywords[:6]: # Use first 6 keywords for each emotion
|
| 414 |
keyword_doc = nlp(keyword)
|
|
|
|
| 419 |
for keyword_token in keyword_doc:
|
| 420 |
similarity = token.similarity(keyword_token)
|
| 421 |
max_similarity = max(max_similarity, similarity)
|
| 422 |
+
|
| 423 |
keyword_scores.append(max_similarity)
|
| 424 |
+
|
| 425 |
# Return average of top 3 similarities if we have at least 3 scores
|
| 426 |
if len(keyword_scores) >= 3:
|
| 427 |
return sum(sorted(keyword_scores, reverse=True)[:3]) / 3
|
|
|
|
| 435 |
"""Calculate emotion score based on similarity, context, and phrase detection"""
|
| 436 |
# Get emotion score using spaCy word vectors
|
| 437 |
similarity_score = calculate_emotion_similarity(text, keywords)
|
| 438 |
+
|
| 439 |
# Check for emotion-specific phrases
|
| 440 |
detected_phrases = detect_phrases(text, {emotion: EMOTION_PHRASES[emotion]})
|
| 441 |
phrase_count = len(detected_phrases.get(emotion, []))
|
| 442 |
phrase_score = min(phrase_count * 0.2, 0.6) # Cap at 0.6
|
| 443 |
+
|
| 444 |
# Get contextual features
|
| 445 |
features = detect_contextual_features(text)
|
| 446 |
+
|
| 447 |
# Calculate feature-based adjustment
|
| 448 |
feature_adjustment = 0
|
| 449 |
+
|
| 450 |
# Search for direct emotion mentions in text
|
| 451 |
doc = nlp(text.lower())
|
| 452 |
direct_mention_score = 0
|
| 453 |
+
|
| 454 |
for token in doc:
|
| 455 |
if token.lemma_ in keywords:
|
| 456 |
direct_mention_score += 0.2 # Direct mention of emotion word
|
| 457 |
break
|
| 458 |
+
|
| 459 |
# Adjust score based on emotional context
|
| 460 |
if emotion in ['joy', 'love', 'surprise'] and features['exclamations'] > 0:
|
| 461 |
feature_adjustment += min(features['exclamations'] * 0.05, 0.2)
|
| 462 |
+
|
| 463 |
if emotion in ['anger', 'sadness'] and features['negators'] > 0:
|
| 464 |
feature_adjustment += min(features['negators'] * 0.05, 0.2)
|
| 465 |
+
|
| 466 |
if emotion == 'fear' and features['intensifiers'] > 0:
|
| 467 |
feature_adjustment += min(features['intensifiers'] * 0.05, 0.2)
|
| 468 |
+
|
| 469 |
# Combine scores with appropriate weights
|
| 470 |
final_score = (similarity_score * 0.5) + (phrase_score * 0.3) + (feature_adjustment * 0.1) + (direct_mention_score * 0.1)
|
| 471 |
+
|
| 472 |
# Normalize to ensure it's in [0, 1]
|
| 473 |
return max(0, min(final_score, 1.0)), detected_phrases.get(emotion, [])
|
| 474 |
|
|
|
|
| 477 |
# 1. Keyword similarity for sarcasm words
|
| 478 |
sarcasm_keywords = EMOTION_CATEGORIES['sarcasm']
|
| 479 |
similarity_score = calculate_emotion_similarity(text, sarcasm_keywords)
|
| 480 |
+
|
| 481 |
# 2. Linguistic pattern detection
|
| 482 |
pattern_score, pattern_matches = detect_sarcasm_patterns(text)
|
| 483 |
+
|
| 484 |
# 3. Check for semantic incongruity between sentences
|
| 485 |
incongruity_score = 0
|
| 486 |
sentences = list(nlp(text).sents)
|
| 487 |
+
|
| 488 |
if len(sentences) > 1:
|
| 489 |
# Calculate similarity between adjacent sentences
|
| 490 |
similarities = []
|
| 491 |
for i in range(len(sentences) - 1):
|
| 492 |
sim = sentences[i].similarity(sentences[i+1])
|
| 493 |
similarities.append(sim)
|
| 494 |
+
|
| 495 |
# Low similarity between adjacent sentences might indicate sarcasm
|
| 496 |
if similarities and min(similarities) < 0.5:
|
| 497 |
incongruity_score = 0.3
|
| 498 |
+
|
| 499 |
# 4. Check for sarcasm phrases
|
| 500 |
detected_phrases = detect_phrases(text, {'sarcasm': EMOTION_PHRASES['sarcasm']})
|
| 501 |
phrase_score = min(len(detected_phrases.get('sarcasm', [])) * 0.2, 0.6)
|
| 502 |
+
|
| 503 |
# 5. Check for emotional contrast
|
| 504 |
# (positive words in negative context or vice versa)
|
| 505 |
doc = nlp(text.lower())
|
| 506 |
+
|
| 507 |
# Count positive and negative words
|
| 508 |
pos_count = 0
|
| 509 |
neg_count = 0
|
| 510 |
+
|
| 511 |
for token in doc:
|
| 512 |
if token.is_alpha and not token.is_stop:
|
| 513 |
# Check against positive and negative emotion keywords
|
|
|
|
| 515 |
pos_count += 1
|
| 516 |
if any(token.similarity(nlp(word)) > 0.7 for word in EMOTION_CATEGORIES['sadness'][:5] + EMOTION_CATEGORIES['anger'][:5]):
|
| 517 |
neg_count += 1
|
| 518 |
+
|
| 519 |
contrast_score = 0
|
| 520 |
if pos_count > 0 and neg_count > 0:
|
| 521 |
contrast_score = min(0.3, pos_count * neg_count * 0.05)
|
| 522 |
+
|
| 523 |
# Weighted combination of all scores
|
| 524 |
combined_score = (0.15 * similarity_score) + (0.35 * pattern_score) + \
|
| 525 |
(0.15 * incongruity_score) + (0.25 * phrase_score) + \
|
| 526 |
(0.1 * contrast_score)
|
| 527 |
+
|
| 528 |
# Normalize to [0, 1]
|
| 529 |
return max(0, min(combined_score, 1.0)), detected_phrases.get('sarcasm', []), pattern_matches
|
| 530 |
|
| 531 |
+
def determine_emotional_verdict(emotion_scores):
|
| 532 |
+
"""Determine the emotional verdict based on the emotional profile"""
|
| 533 |
+
# Create a sorted list of emotions by score
|
| 534 |
+
sorted_emotions = sorted(emotion_scores.items(), key=lambda x: x[1], reverse=True)
|
| 535 |
+
|
| 536 |
+
# Count emotions over different thresholds
|
| 537 |
+
emotions_over_35 = [e for e, s in sorted_emotions if s > 0.35]
|
| 538 |
+
emotions_over_20 = [e for e, s in sorted_emotions if s > 0.20]
|
| 539 |
+
emotions_over_15 = [e for e, s in sorted_emotions if s > 0.15]
|
| 540 |
+
|
| 541 |
+
# Check if we have a strong dominant emotion (over 35%)
|
| 542 |
+
if emotions_over_35:
|
| 543 |
+
dominant_emotion = emotions_over_35[0]
|
| 544 |
+
for verdict_key, verdict_info in EMOTION_VERDICT_CATEGORIES.items():
|
| 545 |
+
conditions = verdict_info['conditions']
|
| 546 |
+
# Check single emotion conditions
|
| 547 |
+
if len(conditions) == 1 and isinstance(conditions[0], tuple):
|
| 548 |
+
emotion, threshold = conditions[0]
|
| 549 |
+
if emotion == dominant_emotion and emotion_scores[emotion] >= threshold:
|
| 550 |
+
return verdict_info['label'], verdict_info['description']
|
| 551 |
+
|
| 552 |
+
# Check for emotion combinations
|
| 553 |
+
for verdict_key, verdict_info in EMOTION_VERDICT_CATEGORIES.items():
|
| 554 |
+
conditions = verdict_info['conditions']
|
| 555 |
+
|
| 556 |
+
# Skip single emotion conditions we've already checked
|
| 557 |
+
if len(conditions) == 1 and isinstance(conditions[0], tuple):
|
| 558 |
+
continue
|
| 559 |
+
|
| 560 |
+
# Handle special condition types
|
| 561 |
+
if conditions == ['multiple_over_15'] and len(emotions_over_15) >= 3:
|
| 562 |
+
return verdict_info['label'], verdict_info['description']
|
| 563 |
+
|
| 564 |
+
if conditions == ['all_under_20'] and not emotions_over_20:
|
| 565 |
+
return verdict_info['label'], verdict_info['description']
|
| 566 |
+
|
| 567 |
+
if conditions == ['all_under_15'] and not emotions_over_15:
|
| 568 |
+
return verdict_info['label'], verdict_info['description']
|
| 569 |
+
|
| 570 |
+
# Check standard combination conditions
|
| 571 |
+
if all(emotion_scores.get(emotion, 0) >= threshold for emotion, threshold in conditions):
|
| 572 |
+
return verdict_info['label'], verdict_info['description']
|
| 573 |
+
|
| 574 |
+
# If we've found nothing specific but have some emotions over 15%
|
| 575 |
+
if emotions_over_15:
|
| 576 |
+
if len(emotions_over_15) == 1:
|
| 577 |
+
# Use the single emotion even though it's not super strong
|
| 578 |
+
emotion = emotions_over_15[0]
|
| 579 |
+
return f"Moderately {emotion.capitalize()}", f"Shows some signs of {emotion}"
|
| 580 |
+
else:
|
| 581 |
+
# Create a custom mixed emotion label
|
| 582 |
+
primary = emotions_over_15[0].capitalize()
|
| 583 |
+
secondary = emotions_over_15[1].capitalize()
|
| 584 |
+
return f"{primary} with {secondary}", f"A mix of {primary.lower()} and {secondary.lower()} emotions"
|
| 585 |
+
|
| 586 |
+
# Default fallback
|
| 587 |
+
return "Neutral or Subtle", "No clear emotional signals detected"
|
| 588 |
+
|
| 589 |
def analyze_emotions(text):
|
| 590 |
+
"""Analyze emotions in text using spaCy with robust sarcasm detection and emotional verdict"""
|
| 591 |
if not text or not text.strip():
|
| 592 |
return None, {"error": "Please enter some text to analyze"}
|
| 593 |
+
|
| 594 |
try:
|
| 595 |
# Calculate scores for each emotion with supporting phrases
|
| 596 |
emotion_data = {}
|
| 597 |
+
|
| 598 |
# For each standard emotion category (excluding sarcasm)
|
| 599 |
for emotion, keywords in EMOTION_CATEGORIES.items():
|
| 600 |
if emotion == 'sarcasm':
|
| 601 |
continue
|
| 602 |
+
|
| 603 |
# Use specialized function to get emotion score and supporting phrases
|
| 604 |
score, phrases = get_emotion_score(text, emotion, keywords)
|
| 605 |
emotion_data[emotion] = {
|
| 606 |
'score': score,
|
| 607 |
'phrases': phrases
|
| 608 |
}
|
| 609 |
+
|
| 610 |
# Special handling for sarcasm with multi-method approach
|
| 611 |
sarcasm_score, sarcasm_phrases, sarcasm_patterns = analyze_sarcasm(text)
|
| 612 |
emotion_data['sarcasm'] = {
|
|
|
|
| 614 |
'phrases': sarcasm_phrases,
|
| 615 |
'patterns': sarcasm_patterns
|
| 616 |
}
|
| 617 |
+
|
| 618 |
# Get contextual features for overall analysis
|
| 619 |
context_features = detect_contextual_features(text)
|
| 620 |
+
|
| 621 |
# Apply decision making for final analysis
|
| 622 |
# 1. Check for dominant emotions by raw scores
|
| 623 |
emotion_scores = {emotion: data['score'] for emotion, data in emotion_data.items()}
|
| 624 |
+
|
| 625 |
# 2. Adjust based on contextual evidence
|
| 626 |
+
# If we have strong phrase evidence, boost the score slightly
|
| 627 |
for emotion, data in emotion_data.items():
|
| 628 |
+
if len(data.get('phrases', [])) > 1:
|
| 629 |
+
emotion_scores[emotion] = min(emotion_scores[emotion] * 1.2, 1.0)
|
| 630 |
+
|
| 631 |
+
# 3. Get emotional verdict
|
| 632 |
+
verdict_label, verdict_description = determine_emotional_verdict(emotion_scores)
|
| 633 |
+
|
| 634 |
+
# 4. Normalize scores to percentages
|
| 635 |
+
total_score = sum(emotion_scores.values()) or 1 # Avoid division by zero
|
| 636 |
+
emotion_percentages = {emotion: (score / total_score) * 100 for emotion, score in emotion_scores.items()}
|
| 637 |
+
|
| 638 |
+
# Sort emotions by percentage for display
|
| 639 |
+
sorted_emotions = sorted(emotion_percentages.items(), key=lambda x: x[1], reverse=True)
|
| 640 |
+
|
| 641 |
+
# Prepare result
|
| 642 |
+
result = {
|
| 643 |
+
'emotion_scores': sorted_emotions,
|
| 644 |
+
'emotional_verdict': {
|
| 645 |
+
'label': verdict_label,
|
| 646 |
+
'description': verdict_description
|
| 647 |
+
},
|
| 648 |
+
'top_emotions': sorted_emotions[:3],
|
| 649 |
+
'supporting_evidence': {
|
| 650 |
+
emotion: data.get('phrases', []) for emotion, data in emotion_data.items() if data.get('phrases')
|
| 651 |
+
},
|
| 652 |
+
'context_features': context_features
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 653 |
}
|
| 654 |
+
|
| 655 |
+
return create_visualization(result), result
|
| 656 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 657 |
except Exception as e:
|
| 658 |
import traceback
|
| 659 |
+
error_msg = traceback.format_exc()
|
| 660 |
+
return None, {"error": f"Analysis error: {str(e)}", "details": error_msg}
|
| 661 |
+
|
| 662 |
+
def create_visualization(result):
|
| 663 |
+
"""Create visualization of emotion analysis results"""
|
| 664 |
+
# Create figure and axis
|
| 665 |
+
fig, ax = plt.subplots(figsize=(10, 6))
|
| 666 |
+
|
| 667 |
+
# Extract emotion data
|
| 668 |
+
emotions = [e[0] for e in result['emotion_scores']]
|
| 669 |
+
scores = [e[1] for e in result['emotion_scores']]
|
| 670 |
+
|
| 671 |
+
# Get colors
|
| 672 |
+
colors = [EMOTION_COLORS.get(emotion, '#CCCCCC') for emotion in emotions]
|
| 673 |
+
|
| 674 |
+
# Create bar chart
|
| 675 |
+
bars = ax.bar(emotions, scores, color=colors)
|
| 676 |
+
|
| 677 |
+
# Add title and labels
|
| 678 |
+
ax.set_title('Emotion Analysis', fontsize=16, fontweight='bold')
|
| 679 |
+
ax.set_xlabel('Emotions', fontsize=12)
|
| 680 |
+
ax.set_ylabel('Score (%)', fontsize=12)
|
| 681 |
+
|
| 682 |
+
# Add verdict as text annotation
|
| 683 |
+
verdict = result['emotional_verdict']['label']
|
| 684 |
+
description = result['emotional_verdict']['description']
|
| 685 |
+
ax.text(0.5, -0.15, f"Verdict: {verdict}", ha='center', transform=ax.transAxes, fontsize=14, fontweight='bold')
|
| 686 |
+
ax.text(0.5, -0.2, f"{description}", ha='center', transform=ax.transAxes, fontsize=12)
|
| 687 |
+
|
| 688 |
+
# Rotate x-axis labels for readability
|
| 689 |
+
plt.xticks(rotation=45, ha='right')
|
| 690 |
+
|
| 691 |
+
# Add value labels on top of bars
|
| 692 |
+
for bar in bars:
|
| 693 |
+
height = bar.get_height()
|
| 694 |
+
ax.text(bar.get_x() + bar.get_width()/2., height + 0.5,
|
| 695 |
+
f'{height:.1f}%', ha='center', va='bottom', fontsize=10)
|
| 696 |
+
|
| 697 |
+
# Adjust layout
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 698 |
plt.tight_layout()
|
| 699 |
+
plt.subplots_adjust(bottom=0.25) # Make room for the verdict text
|
| 700 |
+
|
| 701 |
+
# Return figure
|
| 702 |
return fig
|
| 703 |
|
| 704 |
+
def analyze_text(text):
|
| 705 |
+
"""Analyze text and return visualization and detailed results"""
|
| 706 |
+
fig, result = analyze_emotions(text)
|
| 707 |
+
|
| 708 |
+
if 'error' in result:
|
| 709 |
+
return None, result['error']
|
| 710 |
+
|
| 711 |
+
# Format the results for display
|
| 712 |
+
verdict = result['emotional_verdict']['label']
|
| 713 |
+
description = result['emotional_verdict']['description']
|
| 714 |
+
|
| 715 |
+
# Create a formatted summary
|
| 716 |
+
summary = f"## Emotional Analysis Results\n\n"
|
| 717 |
+
summary += f"**Verdict:** {verdict}\n\n"
|
| 718 |
+
summary += f"**Description:** {description}\n\n"
|
| 719 |
+
|
| 720 |
+
summary += "### Top Emotions:\n"
|
| 721 |
+
for emotion, score in result['top_emotions']:
|
| 722 |
+
summary += f"- {emotion.capitalize()}: {score:.1f}%\n"
|
| 723 |
+
|
| 724 |
+
if result['supporting_evidence']:
|
| 725 |
+
summary += "\n### Supporting Evidence:\n"
|
| 726 |
+
for emotion, phrases in result['supporting_evidence'].items():
|
| 727 |
+
if phrases:
|
| 728 |
+
summary += f"- **{emotion.capitalize()}**: {', '.join(phrases)}\n"
|
| 729 |
+
|
| 730 |
+
return fig, summary
|
| 731 |
+
|
| 732 |
# Create Gradio interface
|
| 733 |
+
def create_interface():
|
| 734 |
+
with gr.Blocks(title="Emotional Analysis Tool") as demo:
|
| 735 |
+
gr.Markdown("# Advanced Emotion Analysis")
|
| 736 |
+
gr.Markdown("Enter text to analyze the emotional content and receive a detailed breakdown.")
|
| 737 |
+
|
| 738 |
+
with gr.Row():
|
| 739 |
+
with gr.Column(scale=2):
|
| 740 |
+
text_input = gr.Textbox(
|
| 741 |
+
label="Text to analyze",
|
| 742 |
+
placeholder="Enter text here...",
|
| 743 |
+
lines=10
|
| 744 |
+
)
|
| 745 |
+
analyze_button = gr.Button("Analyze Emotions")
|
| 746 |
+
|
| 747 |
+
with gr.Column(scale=3):
|
| 748 |
+
with gr.Tab("Visualization"):
|
| 749 |
+
plot_output = gr.Plot(label="Emotion Distribution")
|
| 750 |
+
with gr.Tab("Summary"):
|
| 751 |
+
text_output = gr.Markdown(label="Analysis Summary")
|
| 752 |
+
|
| 753 |
+
analyze_button.click(
|
| 754 |
+
fn=analyze_text,
|
| 755 |
+
inputs=text_input,
|
| 756 |
+
outputs=[plot_output, text_output]
|
| 757 |
+
)
|
| 758 |
+
|
| 759 |
+
gr.Markdown("""
|
| 760 |
+
## About This Tool
|
| 761 |
+
|
| 762 |
+
This advanced emotion analysis tool uses NLP techniques to detect and analyze emotions in text.
|
| 763 |
+
It can identify:
|
| 764 |
+
- 10 different emotions (joy, sadness, anger, fear, surprise, love, sarcasm, disgust, anticipation, trust)
|
| 765 |
+
- Complex emotional combinations
|
| 766 |
+
- Contextual features that affect emotional interpretation
|
| 767 |
+
- Intelligent emotional verdicts for mixed emotion states
|
| 768 |
+
|
| 769 |
+
The model uses word vectors, phrase detection, and contextual analysis for accurate emotion recognition.
|
| 770 |
+
""")
|
| 771 |
+
|
| 772 |
+
return demo
|
| 773 |
|
| 774 |
+
# Launch the interface if running directly
|
| 775 |
if __name__ == "__main__":
|
| 776 |
+
print("Creating Gradio interface...")
|
| 777 |
+
demo = create_interface()
|
| 778 |
+
demo.launch(share=True)
|
| 779 |
+
print("Gradio interface launched!")
|