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