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