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