Spaces:
Running
Running
Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
import pandas as pd
|
3 |
+
from collections import Counter
|
4 |
+
from typing import List, Dict, Optional, Tuple
|
5 |
+
import logging
|
6 |
+
|
7 |
+
from config import config
|
8 |
+
from models import SentimentEngine, memory_cleanup, ThemeContext, handle_errors
|
9 |
+
from analysis import AdvancedAnalysisEngine
|
10 |
+
from visualization import PlotlyVisualizer
|
11 |
+
from data_utils import HistoryManager, DataHandler, TextProcessor
|
12 |
+
|
13 |
+
logger = logging.getLogger(__name__)
|
14 |
+
|
15 |
+
class SentimentApp:
|
16 |
+
"""Optimized multilingual sentiment analysis application"""
|
17 |
+
|
18 |
+
def __init__(self):
|
19 |
+
self.engine = SentimentEngine()
|
20 |
+
self.advanced_engine = AdvancedAnalysisEngine()
|
21 |
+
self.history = HistoryManager()
|
22 |
+
self.data_handler = DataHandler()
|
23 |
+
|
24 |
+
# Multi-language examples
|
25 |
+
self.examples = [
|
26 |
+
# Auto Detect
|
27 |
+
["The film had its moments, but overall it felt a bit too long and lacked emotional depth. Some scenes were visually impressive, yet they failed to connect emotionally. By the end, I found myself disengaged and unsatisfied."],
|
28 |
+
|
29 |
+
# English
|
30 |
+
["I was completely blown away by the movie — the performances were raw and powerful, and the story stayed with me long after the credits rolled. Every scene felt purposeful, and the emotional arc was handled with incredible nuance. It's the kind of film that makes you reflect deeply on your own life."],
|
31 |
+
|
32 |
+
# Chinese
|
33 |
+
["这部电影节奏拖沓,剧情老套,完全没有让我产生任何共鸣,是一次失望的观影体验。演员的表演也显得做作,缺乏真实感。看到最后甚至有点不耐烦,整体表现乏善可陈。"],
|
34 |
+
# Spanish
|
35 |
+
["Una obra maestra del cine contemporáneo, con actuaciones sobresalientes, un guion bien escrito y una dirección impecable. Cada plano parecía cuidadosamente pensado, y la historia avanzaba con una intensidad emocional que mantenía al espectador cautivado. Definitivamente una película que vale la pena volver a ver."],
|
36 |
+
# French
|
37 |
+
["Je m'attendais à beaucoup mieux. Le scénario était confus, les dialogues ennuyeux, et je me suis presque endormi au milieu du film. Même la mise en scène, habituellement un point fort, manquait cruellement d'inspiration cette fois-ci."],
|
38 |
+
# German
|
39 |
+
["Der Film war ein emotionales Erlebnis mit großartigen Bildern, einem mitreißenden Soundtrack und einer Geschichte, die zum Nachdenken anregt. Besonders beeindruckend war die schauspielerische Leistung der Hauptdarsteller, die eine tiefe Menschlichkeit vermittelten. Es ist ein Film, der lange nachwirkt."],
|
40 |
+
# Swedish
|
41 |
+
["Filmen var en besvikelse – tråkig handling, överdrivet skådespeleri och ett slut som inte gav något avslut alls. Den kändes forcerad och saknade en tydlig röd tråd. Jag gick från biografen med en känsla av tomhet och frustration."]
|
42 |
+
]
|
43 |
+
|
44 |
+
@handle_errors(default_return=("Please enter text", None, None))
|
45 |
+
def analyze_single(self, text: str, language: str, theme: str, clean_text: bool,
|
46 |
+
remove_punct: bool, remove_nums: bool):
|
47 |
+
"""Optimized single text analysis"""
|
48 |
+
if not text.strip():
|
49 |
+
return "Please enter text", None, None
|
50 |
+
|
51 |
+
# Map display names to language codes
|
52 |
+
language_map = {v: k for k, v in config.SUPPORTED_LANGUAGES.items()}
|
53 |
+
language_code = language_map.get(language, 'auto')
|
54 |
+
|
55 |
+
preprocessing_options = {
|
56 |
+
'clean_text': clean_text,
|
57 |
+
'remove_punctuation': remove_punct,
|
58 |
+
'remove_numbers': remove_nums
|
59 |
+
}
|
60 |
+
|
61 |
+
with memory_cleanup():
|
62 |
+
result = self.engine.analyze_single(text, language_code, preprocessing_options)
|
63 |
+
|
64 |
+
# Add to history
|
65 |
+
history_entry = {
|
66 |
+
'text': text[:100] + '...' if len(text) > 100 else text,
|
67 |
+
'full_text': text,
|
68 |
+
'sentiment': result['sentiment'],
|
69 |
+
'confidence': result['confidence'],
|
70 |
+
'pos_prob': result.get('pos_prob', 0),
|
71 |
+
'neg_prob': result.get('neg_prob', 0),
|
72 |
+
'neu_prob': result.get('neu_prob', 0),
|
73 |
+
'language': result['language'],
|
74 |
+
'word_count': result['word_count'],
|
75 |
+
'analysis_type': 'single'
|
76 |
+
}
|
77 |
+
self.history.add(history_entry)
|
78 |
+
|
79 |
+
# Create visualizations
|
80 |
+
theme_ctx = ThemeContext(theme)
|
81 |
+
gauge_fig = PlotlyVisualizer.create_sentiment_gauge(result, theme_ctx)
|
82 |
+
bars_fig = PlotlyVisualizer.create_probability_bars(result, theme_ctx)
|
83 |
+
|
84 |
+
# Create comprehensive result text
|
85 |
+
info_text = f"""
|
86 |
+
**Analysis Results:**
|
87 |
+
- **Sentiment:** {result['sentiment']} ({result['confidence']:.3f} confidence)
|
88 |
+
- **Language:** {result['language'].upper()}
|
89 |
+
- **Statistics:** {result['word_count']} words, {result['char_count']} characters
|
90 |
+
- **Probabilities:** Positive: {result.get('pos_prob', 0):.3f}, Negative: {result.get('neg_prob', 0):.3f}, Neutral: {result.get('neu_prob', 0):.3f}
|
91 |
+
"""
|
92 |
+
|
93 |
+
return info_text, gauge_fig, bars_fig
|
94 |
+
|
95 |
+
@handle_errors(default_return=("Please enter texts", None, None, None))
|
96 |
+
def analyze_batch(self, batch_text: str, language: str, theme: str,
|
97 |
+
clean_text: bool, remove_punct: bool, remove_nums: bool):
|
98 |
+
"""Enhanced batch analysis with parallel processing"""
|
99 |
+
if not batch_text.strip():
|
100 |
+
return "Please enter texts (one per line)", None, None, None
|
101 |
+
|
102 |
+
# Parse batch input
|
103 |
+
texts = TextProcessor.parse_batch_input(batch_text)
|
104 |
+
if len(texts) > config.BATCH_SIZE_LIMIT:
|
105 |
+
return f"Too many texts. Maximum {config.BATCH_SIZE_LIMIT} allowed.", None, None, None
|
106 |
+
|
107 |
+
if not texts:
|
108 |
+
return "No valid texts found", None, None, None
|
109 |
+
|
110 |
+
# Map display names to language codes
|
111 |
+
language_map = {v: k for k, v in config.SUPPORTED_LANGUAGES.items()}
|
112 |
+
language_code = language_map.get(language, 'auto')
|
113 |
+
|
114 |
+
preprocessing_options = {
|
115 |
+
'clean_text': clean_text,
|
116 |
+
'remove_punctuation': remove_punct,
|
117 |
+
'remove_numbers': remove_nums
|
118 |
+
}
|
119 |
+
|
120 |
+
with memory_cleanup():
|
121 |
+
results = self.engine.analyze_batch(texts, language_code, preprocessing_options)
|
122 |
+
|
123 |
+
# Add to history
|
124 |
+
batch_entries = []
|
125 |
+
for result in results:
|
126 |
+
if 'error' not in result:
|
127 |
+
entry = {
|
128 |
+
'text': result['text'],
|
129 |
+
'full_text': result['full_text'],
|
130 |
+
'sentiment': result['sentiment'],
|
131 |
+
'confidence': result['confidence'],
|
132 |
+
'pos_prob': result.get('pos_prob', 0),
|
133 |
+
'neg_prob': result.get('neg_prob', 0),
|
134 |
+
'neu_prob': result.get('neu_prob', 0),
|
135 |
+
'language': result['language'],
|
136 |
+
'word_count': result['word_count'],
|
137 |
+
'analysis_type': 'batch',
|
138 |
+
'batch_index': result['batch_index']
|
139 |
+
}
|
140 |
+
batch_entries.append(entry)
|
141 |
+
|
142 |
+
self.history.add_batch(batch_entries)
|
143 |
+
|
144 |
+
# Create visualizations
|
145 |
+
theme_ctx = ThemeContext(theme)
|
146 |
+
summary_fig = PlotlyVisualizer.create_batch_summary(results, theme_ctx)
|
147 |
+
confidence_fig = PlotlyVisualizer.create_confidence_distribution(results)
|
148 |
+
|
149 |
+
# Create results DataFrame
|
150 |
+
df_data = []
|
151 |
+
for result in results:
|
152 |
+
if 'error' in result:
|
153 |
+
df_data.append({
|
154 |
+
'Index': result['batch_index'] + 1,
|
155 |
+
'Text': result['text'],
|
156 |
+
'Sentiment': 'Error',
|
157 |
+
'Confidence': 0.0,
|
158 |
+
'Language': 'Unknown',
|
159 |
+
'Error': result['error']
|
160 |
+
})
|
161 |
+
else:
|
162 |
+
df_data.append({
|
163 |
+
'Index': result['batch_index'] + 1,
|
164 |
+
'Text': result['text'],
|
165 |
+
'Sentiment': result['sentiment'],
|
166 |
+
'Confidence': f"{result['confidence']:.3f}",
|
167 |
+
'Language': result['language'].upper(),
|
168 |
+
'Word_Count': result.get('word_count', 0)
|
169 |
+
})
|
170 |
+
|
171 |
+
df = pd.DataFrame(df_data)
|
172 |
+
|
173 |
+
# Create summary text
|
174 |
+
successful_results = [r for r in results if 'error' not in r]
|
175 |
+
error_count = len(results) - len(successful_results)
|
176 |
+
|
177 |
+
if successful_results:
|
178 |
+
sentiment_counts = Counter([r['sentiment'] for r in successful_results])
|
179 |
+
avg_confidence = np.mean([r['confidence'] for r in successful_results])
|
180 |
+
languages = Counter([r['language'] for r in successful_results])
|
181 |
+
|
182 |
+
summary_text = f"""
|
183 |
+
**Batch Analysis Summary:**
|
184 |
+
- **Total Texts:** {len(texts)}
|
185 |
+
- **Successful:** {len(successful_results)}
|
186 |
+
- **Errors:** {error_count}
|
187 |
+
- **Average Confidence:** {avg_confidence:.3f}
|
188 |
+
- **Sentiments:** {dict(sentiment_counts)}
|
189 |
+
- **Languages Detected:** {dict(languages)}
|
190 |
+
"""
|
191 |
+
else:
|
192 |
+
summary_text = f"All {len(texts)} texts failed to analyze."
|
193 |
+
|
194 |
+
return summary_text, df, summary_fig, confidence_fig
|
195 |
+
|
196 |
+
# FIXED advanced analysis methods with sample size control
|
197 |
+
@handle_errors(default_return=("Please enter text", None))
|
198 |
+
def analyze_with_shap(self, text: str, language: str, num_samples: int = 100):
|
199 |
+
"""Perform FIXED SHAP analysis with configurable samples"""
|
200 |
+
language_map = {v: k for k, v in config.SUPPORTED_LANGUAGES.items()}
|
201 |
+
language_code = language_map.get(language, 'auto')
|
202 |
+
|
203 |
+
return self.advanced_engine.analyze_with_shap(text, language_code, num_samples)
|
204 |
+
|
205 |
+
@handle_errors(default_return=("Please enter text", None))
|
206 |
+
def analyze_with_lime(self, text: str, language: str, num_samples: int = 100):
|
207 |
+
"""Perform FIXED LIME analysis with configurable samples"""
|
208 |
+
language_map = {v: k for k, v in config.SUPPORTED_LANGUAGES.items()}
|
209 |
+
language_code = language_map.get(language, 'auto')
|
210 |
+
|
211 |
+
return self.advanced_engine.analyze_with_lime(text, language_code, num_samples)
|
212 |
+
|
213 |
+
@handle_errors(default_return=(None, "No history available"))
|
214 |
+
def plot_history(self, theme: str = 'default'):
|
215 |
+
"""Plot comprehensive history analysis"""
|
216 |
+
history = self.history.get_all()
|
217 |
+
if len(history) < 2:
|
218 |
+
return None, f"Need at least 2 analyses for trends. Current: {len(history)}"
|
219 |
+
|
220 |
+
theme_ctx = ThemeContext(theme)
|
221 |
+
|
222 |
+
with memory_cleanup():
|
223 |
+
fig = PlotlyVisualizer.create_history_dashboard(history, theme_ctx)
|
224 |
+
stats = self.history.get_stats()
|
225 |
+
|
226 |
+
stats_text = f"""
|
227 |
+
**History Statistics:**
|
228 |
+
- **Total Analyses:** {stats.get('total_analyses', 0)}
|
229 |
+
- **Positive:** {stats.get('positive_count', 0)}
|
230 |
+
- **Negative:** {stats.get('negative_count', 0)}
|
231 |
+
- **Neutral:** {stats.get('neutral_count', 0)}
|
232 |
+
- **Average Confidence:** {stats.get('avg_confidence', 0):.3f}
|
233 |
+
- **Languages:** {stats.get('languages_detected', 0)}
|
234 |
+
- **Most Common Language:** {stats.get('most_common_language', 'N/A').upper()}
|
235 |
+
"""
|
236 |
+
|
237 |
+
return fig, stats_text
|
238 |
+
|
239 |
+
@handle_errors(default_return=("No data available",))
|
240 |
+
def get_history_status(self):
|
241 |
+
"""Get current history status"""
|
242 |
+
stats = self.history.get_stats()
|
243 |
+
if not stats:
|
244 |
+
return "No analyses performed yet"
|
245 |
+
|
246 |
+
return f"""
|
247 |
+
**Current Status:**
|
248 |
+
- **Total Analyses:** {stats['total_analyses']}
|
249 |
+
- **Recent Sentiment Distribution:**
|
250 |
+
* Positive: {stats['positive_count']}
|
251 |
+
* Negative: {stats['negative_count']}
|
252 |
+
* Neutral: {stats['neutral_count']}
|
253 |
+
- **Average Confidence:** {stats['avg_confidence']:.3f}
|
254 |
+
- **Languages Detected:** {stats['languages_detected']}
|
255 |
+
"""
|