| """ |
| 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]) |
| |
| if isinstance(results[0], list): |
| results = results[0] |
| emotions = {res["label"]: res["score"] for res in results} |
| |
| dominant = max(emotions, key=emotions.get) |
| score = emotions[dominant] |
|
|
| |
| |
| |
| 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 = {"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_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: |
| |
| for i, w in enumerate(words): |
| if w in negation_words: |
| window = words[i+1 : i+4] |
| if any(neg in window for neg in strong_negatives): |
| |
| neutral_score = min(score * 0.8, 0.75) |
| emotions["neutral"] = neutral_score |
| return "neutral", neutral_score, emotions |
|
|
| |
| 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: |
| |
| 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_map = { |
| "joy": round(65 + (score * 34)), |
| "surprise": round(55 + (score * 30)), |
| "neutral": round(35 + (score * 20)), |
| "sadness": round(5 + (score * 20)), |
| "anger": round(10 + (score * 25)), |
| "fear": round(10 + (score * 20)), |
| "disgust": round(5 + (score * 15)), |
| } |
| eng = engagement_map.get(dominant, 40) |
|
|
| |
| positive_emotions = ["joy", "surprise"] |
| negative_emotions = ["anger", "disgust", "fear", "sadness"] |
|
|
| |
| |
| if dominant in positive_emotions: |
| sentiment = "POSITIVE" |
| sentiment_score = round(score, 2) |
| elif dominant in negative_emotions: |
| sentiment = "NEGATIVE" |
| sentiment_score = round(score, 2) |
| else: |
| sentiment = "NEUTRAL" |
| sentiment_score = round(score, 2) |
|
|
| |
| 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") |
|
|