File size: 8,205 Bytes
099d157 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | """
Text Sentiment & Emotion β High Precision Emotion Engine
Uses j-hartmann/emotion-english-distilroberta-base (7-class).
Verified labels: anger, disgust, fear, joy, neutral, sadness, surprise
"""
_classifier = None
_mode = None
def get_classifier():
global _classifier, _mode
if _classifier is not None:
return _classifier
try:
from transformers import pipeline as hf_pipeline
import torch
device = 0 if torch.cuda.is_available() else -1
_classifier = hf_pipeline(
"text-classification",
model="j-hartmann/emotion-english-distilroberta-base",
top_k=None,
device=device
)
_mode = "transformers"
print("[TextModel] HuggingFace precise emotion engine loaded: emotion-english-distilroberta-base (7-class)")
except Exception as e:
print(f"[TextModel] HuggingFace unavailable ({e}), using keyword heuristic")
_classifier = "HEURISTIC"
_mode = "heuristic"
return _classifier
def analyze_text(text):
if not text:
return _empty_result()
word_count = len(text.split())
classifier = get_classifier()
if _mode == "transformers" and classifier != "HEURISTIC":
try:
results = classifier(text[:1500])
# top_k=None returns a list of dicts directly
if isinstance(results[0], list):
results = results[0]
emotions = {res["label"]: res["score"] for res in results}
# Find dominant emotion
dominant = max(emotions, key=emotions.get)
score = emotions[dominant]
# ββ Negation Neutralizer ββββββββββββββββββββββββββββββββββββββ
# If the model outputs a strong NEGATIVE emotion but the sentence
# contains clear negation words before negative content, downgrade to neutral.
dominant, score, emotions = _apply_negation_check(text, dominant, score, emotions)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
return _format_emotion_output(dominant, score, emotions, word_count, "DistilRoBERTa-Emotion")
except Exception as e:
print(f"Text Model Error: {e}")
import traceback
traceback.print_exc()
return _keyword_classify(text, word_count)
else:
return _keyword_classify(text, word_count)
def _apply_negation_check(text, dominant, score, emotions):
"""
Post-process the ML result to detect negated negative phrases.
If the model said NEGATIVE but the sentence is clearly negating that negativity
(e.g. 'not going to hell', 'don't hate'), override to Neutral.
Also handles mixed sentences (happy + sad) by checking balance.
"""
text_lower = text.lower()
words = text_lower.split()
# Negation words that flip meaning
negation_words = {"not", "no", "never", "don't", "dont", "won't", "wont",
"can't", "cant", "isn't", "isnt", "aren't", "arent",
"wouldn't", "wouldnt", "shouldn't", "shouldnt", "didn't", "didnt"}
# Strong negative content words (things that sound bad but may be negated)
strong_negatives = {"hell", "die", "death", "dead", "hate", "terrible",
"awful", "horrible", "evil", "hurt", "kill", "murder",
"suffer", "pain", "miserable", "disaster", "fail", "failure"}
negative_emotions = {"anger", "disgust", "fear", "sadness"}
if dominant in negative_emotions:
# Check: is there a negation word within 3 words before a strong negative?
for i, w in enumerate(words):
if w in negation_words:
window = words[i+1 : i+4] # next 3 words after negation
if any(neg in window for neg in strong_negatives):
# Negated negative β override to neutral
neutral_score = min(score * 0.8, 0.75)
emotions["neutral"] = neutral_score
return "neutral", neutral_score, emotions
# Mixed sentence check: contains both strong positive AND negative content
positive_words = {"happy", "love", "great", "good", "joy", "excited",
"wonderful", "amazing", "glad", "pleased", "enjoy"}
negative_words = {"sad", "angry", "hate", "fear", "bad", "terrible",
"upset", "awful", "hurt", "cry", "depressed"}
has_positive = any(w in words for w in positive_words)
has_negative = any(w in words for w in negative_words)
if has_positive and has_negative and dominant in negative_emotions:
# Mixed sentence β reduce confidence and lean toward neutral
neutral_score = score * 0.6
emotions["neutral"] = neutral_score
if neutral_score > score * 0.55:
return "neutral", neutral_score, emotions
return dominant, score, emotions
def _format_emotion_output(dominant, score, all_emotions, word_count, provider):
# Engagement scores that LOGICALLY match each emotion
# High engagement = joy/surprise. Low = sadness/fear/disgust/anger. Medium = neutral
engagement_map = {
"joy": round(65 + (score * 34)), # 65-99% β very engaged
"surprise": round(55 + (score * 30)), # 55-85% β alert/engaged
"neutral": round(35 + (score * 20)), # 35-55% β baseline
"sadness": round(5 + (score * 20)), # 5-25% β disengaged/distressed
"anger": round(10 + (score * 25)), # 10-35% β agitated but not engaged
"fear": round(10 + (score * 20)), # 10-30% β withdrawn
"disgust": round(5 + (score * 15)), # 5-20% β very disengaged
}
eng = engagement_map.get(dominant, 40)
# Sentiment polarity
positive_emotions = ["joy", "surprise"]
negative_emotions = ["anger", "disgust", "fear", "sadness"]
# sentiment_score: represent the actual MODEL CONFIDENCE (0.0 to 1.0)
# displayed in the UI as a percentage of how strongly this emotion was detected
if dominant in positive_emotions:
sentiment = "POSITIVE"
sentiment_score = round(score, 2) # e.g. 0.97 β displayed as 97%
elif dominant in negative_emotions:
sentiment = "NEGATIVE"
sentiment_score = round(score, 2) # e.g. 0.91 β displayed as 91%
else:
sentiment = "NEUTRAL"
sentiment_score = round(score, 2)
# Format percentages for UI log
formatted_emotions = {k: round(v * 100) for k, v in all_emotions.items() if v > 0.01}
return {
"sentiment": sentiment,
"sentiment_score": sentiment_score,
"emotions": formatted_emotions,
"engagement_score": eng,
"provider": provider,
"word_count": word_count,
"subjectivity": round(score, 2),
"dominant_emotion": dominant.capitalize()
}
def _empty_result():
return {
"sentiment": "NEUTRAL",
"sentiment_score": 0.5,
"emotions": {},
"engagement_score": 50,
"word_count": 0,
"subjectivity": 0.0,
"provider": "None"
}
def batch_analyze(texts):
return [analyze_text(t) for t in texts]
def _keyword_classify(text, word_count):
"""Fast keyword-based precise fallback."""
text_lower = text.lower()
positive = ["good", "great", "excellent", "happy", "love", "awesome"]
negative = ["bad", "terrible", "awful", "hate", "angry", "sad"]
pos = sum(1 for w in positive if w in text_lower)
neg = sum(1 for w in negative if w in text_lower)
if pos > neg:
return _format_emotion_output("joy", min(0.6 + pos*0.1, 0.99), {"joy":0.8, "neutral":0.2}, word_count, "Heuristic")
elif neg > pos:
return _format_emotion_output("sadness", min(0.6 + neg*0.1, 0.99), {"sadness":0.8, "neutral":0.2}, word_count, "Heuristic")
else:
return _format_emotion_output("neutral", 0.7, {"neutral": 0.8}, word_count, "Heuristic")
|