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!")
|