File size: 4,106 Bytes
f3997d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from transformers import AutoModelForSequenceClassification, AutoTokenizer, AutoConfig
import numpy as np
from scipy.special import softmax
from functools import lru_cache

# Use a better sentiment model trained on Twitter/news (not movie reviews!)
MODEL = "cardiffnlp/twitter-roberta-base-sentiment-latest"

@lru_cache()
def load_sentiment_model():
    """Load and cache the sentiment model and tokenizer."""
    tokenizer = AutoTokenizer.from_pretrained(MODEL)
    config = AutoConfig.from_pretrained(MODEL)
    model = AutoModelForSequenceClassification.from_pretrained(MODEL)
    return tokenizer, config, model

def analyze_sentiment(text: str) -> str:
    """
    Analyze sentiment using Twitter-RoBERTa (better for news/social media).
    
    Args:
        text: Text to analyze
        
    Returns:
        Sentiment classification: 'Positive', 'Negative', or 'Neutral'
    """
    if not text or not text.strip():
        return "Neutral"
    
    try:
        tokenizer, config, model = load_sentiment_model()
        
        # Tokenize and get prediction
        encoded_input = tokenizer(text[:512], return_tensors='pt', truncation=True, max_length=512)
        output = model(**encoded_input)
        scores = output[0][0].detach().numpy()
        scores = softmax(scores)
        
        # Get label with highest score
        # labels: ['negative', 'neutral', 'positive']
        ranking = np.argsort(scores)[::-1]
        label_index = ranking[0]
        confidence = scores[label_index]
        
        # Map to our format
        labels = ['Negative', 'Neutral', 'Positive']
        result = labels[label_index]
        
        # Only return Positive/Negative if confidence > 50%
        # Otherwise return Neutral
        if confidence > 0.5:
            return result
        else:
            return "Neutral"
        
    except Exception as e:
        print(f"❌ Sentiment analysis failed: {e}")
        # Fallback to keyword-based
        return _keyword_sentiment(text)


def _keyword_sentiment(text: str) -> str:
    """Fallback keyword-based sentiment for construction/legal news."""
    text_lower = text.lower()
    
    # Negative keywords for construction/legal news
    negative_words = [
        'illegal', 'scam', 'fraud', 'violation', 'criticise', 'criticize', 
        'fine', 'penalty', 'halted', 'stopped', 'delay', 'problem', 'issue',
        'allege', 'complaint', 'reject', 'denied', 'unsafe', 'danger',
        'cost overrun', 'budget exceed', 'dispute', 'litigation', 'demolition',
        'encroachment', 'unauthorised', 'unauthorized', 'fail', 'failed'
    ]
    
    # Positive keywords
    positive_words = [
        'approve', 'approved', 'success', 'complete', 'completed', 'inaugurate',
        'new project', 'development', 'growth', 'expansion', 'modern', 'upgrade',
        'improvement', 'benefit', 'efficient', 'green', 'sustainable', 'award',
        'milestone', 'breakthrough', 'innovation', 'reduce penalty'
    ]
    
    neg_count = sum(1 for word in negative_words if word in text_lower)
    pos_count = sum(1 for word in positive_words if word in text_lower)
    
    if neg_count > pos_count and neg_count > 0:
        return "Negative"
    elif pos_count > neg_count and pos_count > 0:
        return "Positive"
    else:
        return "Neutral"


def get_sentiment_score(text: str) -> float:
    """
    Get numerical sentiment score.
    
    Args:
        text: Text to analyze
        
    Returns:
        Score between -1 (negative) and 1 (positive)
    """
    if not text or not text.strip():
        return 0.0
    
    try:
        tokenizer, config, model = load_sentiment_model()
        
        encoded_input = tokenizer(text[:512], return_tensors='pt', truncation=True, max_length=512)
        output = model(**encoded_input)
        scores = output[0][0].detach().numpy()
        scores = softmax(scores)
        
        # Convert to -1 to 1 scale
        # scores[0] = negative, scores[1] = neutral, scores[2] = positive
        return float(scores[2] - scores[0])
        
    except Exception:
        return 0.0