isha0110 commited on
Commit
d6bc7f1
·
verified ·
1 Parent(s): 79165cb

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +913 -0
app.py ADDED
@@ -0,0 +1,913 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import torch
3
+ import torch.nn as nn
4
+ from transformers import AutoTokenizer, AutoModel
5
+ import numpy as np
6
+ import os
7
+ from typing import Dict, List, Tuple, Optional
8
+ import json
9
+ from datetime import datetime
10
+ from collections import defaultdict
11
+
12
+ print("🎭 Emotion Classifier Starting...")
13
+
14
+ # ========== CONFIG ==========
15
+ MODEL_NAME = "roberta-base"
16
+ EMOTIONS = ["anger", "fear", "joy", "sadness", "surprise"]
17
+ BEST_THRESHOLDS = [0.24722222, 0.61666667, 0.59722222, 0.44166667, 0.46111111]
18
+ MAX_LEN = 200
19
+ MODEL_PATH = "roberta.pth"
20
+
21
+ # Emotion metadata with richer information
22
+ EMOTION_META = {
23
+ "anger": {
24
+ "emoji": "😠", "color": "#ef4444", "light_color": "#fee2e2",
25
+ "gradient": "linear-gradient(135deg, #ef4444 0%, #dc2626 100%)",
26
+ "description": "Frustration, irritation, or rage",
27
+ "keywords": ["angry", "furious", "mad", "annoyed", "irritated"],
28
+ "intensity_labels": ["Mild irritation", "Moderate anger", "Strong anger", "Intense rage"]
29
+ },
30
+ "fear": {
31
+ "emoji": "😨", "color": "#8b5cf6", "light_color": "#ede9fe",
32
+ "gradient": "linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)",
33
+ "description": "Anxiety, worry, or terror",
34
+ "keywords": ["scared", "afraid", "terrified", "anxious", "worried"],
35
+ "intensity_labels": ["Slight concern", "Moderate fear", "Strong fear", "Extreme terror"]
36
+ },
37
+ "joy": {
38
+ "emoji": "😊", "color": "#fbbf24", "light_color": "#fef3c7",
39
+ "gradient": "linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%)",
40
+ "description": "Happiness, excitement, or delight",
41
+ "keywords": ["happy", "excited", "delighted", "joyful", "thrilled"],
42
+ "intensity_labels": ["Mild pleasure", "Moderate happiness", "Strong joy", "Intense euphoria"]
43
+ },
44
+ "sadness": {
45
+ "emoji": "😢", "color": "#3b82f6", "light_color": "#dbeafe",
46
+ "gradient": "linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)",
47
+ "description": "Sorrow, grief, or disappointment",
48
+ "keywords": ["sad", "depressed", "unhappy", "miserable", "heartbroken"],
49
+ "intensity_labels": ["Mild sadness", "Moderate sorrow", "Strong grief", "Deep despair"]
50
+ },
51
+ "surprise": {
52
+ "emoji": "😲", "color": "#ec4899", "light_color": "#fce7f3",
53
+ "gradient": "linear-gradient(135deg, #ec4899 0%, #db2777 100%)",
54
+ "description": "Astonishment, shock, or amazement",
55
+ "keywords": ["surprised", "shocked", "amazed", "astonished", "startled"],
56
+ "intensity_labels": ["Mild surprise", "Moderate shock", "Strong astonishment", "Complete disbelief"]
57
+ }
58
+ }
59
+
60
+ # ========== MODEL CLASS ==========
61
+ class RobertaEmotion(nn.Module):
62
+ def __init__(self):
63
+ super().__init__()
64
+ self.backbone = AutoModel.from_pretrained(MODEL_NAME)
65
+ self.dropout = nn.Dropout(0.35)
66
+ self.head = nn.Linear(768, 5)
67
+
68
+ def forward(self, input_ids, attention_mask):
69
+ outputs = self.backbone(input_ids=input_ids, attention_mask=attention_mask)
70
+ pooled = outputs.pooler_output if hasattr(outputs, "pooler_output") else outputs.last_hidden_state[:, 0]
71
+ x = self.dropout(pooled)
72
+ return self.head(x)
73
+
74
+ # ========== GLOBAL STATE ==========
75
+ class ModelState:
76
+ def __init__(self):
77
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
78
+ self.model = None
79
+ self.tokenizer = None
80
+ self.ready = False
81
+ self.predictions_count = 0
82
+ self.history = []
83
+ self.emotion_stats = defaultdict(int)
84
+
85
+ state = ModelState()
86
+
87
+ # ========== UTILITY FUNCTIONS ==========
88
+ def sigmoid(x: np.ndarray) -> np.ndarray:
89
+ """Apply sigmoid with numerical stability"""
90
+ return 1 / (1 + np.exp(-np.clip(x, -500, 500)))
91
+
92
+ def get_intensity_level(prob: float) -> Tuple[str, str]:
93
+ """Get intensity level and color"""
94
+ if prob >= 0.85: return "Very High", "#10b981"
95
+ elif prob >= 0.70: return "High", "#3b82f6"
96
+ elif prob >= 0.50: return "Moderate", "#f59e0b"
97
+ elif prob >= 0.30: return "Low", "#9ca3af"
98
+ else: return "Very Low", "#d1d5db"
99
+
100
+ def get_emotion_intensity_label(emotion: str, prob: float) -> str:
101
+ """Get human-readable intensity for emotion"""
102
+ labels = EMOTION_META[emotion]["intensity_labels"]
103
+ if prob >= 0.85: return labels[3]
104
+ elif prob >= 0.65: return labels[2]
105
+ elif prob >= 0.45: return labels[1]
106
+ else: return labels[0]
107
+
108
+ # ========== MODEL LOADING ==========
109
+ def load_model() -> Tuple[str, str]:
110
+ """Load model and return status HTML"""
111
+ try:
112
+ print("📦 Loading model...")
113
+ state.model = RobertaEmotion()
114
+
115
+ if os.path.exists(MODEL_PATH):
116
+ state.model.load_state_dict(torch.load(MODEL_PATH, map_location=state.device))
117
+ print("✅ Trained model loaded")
118
+ status = "success"
119
+ status_text = "Trained Model Loaded"
120
+ else:
121
+ print("⚠️ No trained weights found")
122
+ status = "warning"
123
+ status_text = "Model Initialized (No Weights)"
124
+
125
+ state.model.to(state.device)
126
+ state.model.eval()
127
+ state.tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
128
+ state.ready = True
129
+
130
+ device_emoji = "🚀" if state.device == "cuda" else "💻"
131
+ bg_color = "#d1fae5" if status == "success" else "#fef3c7"
132
+ border_color = "#10b981" if status == "success" else "#f59e0b"
133
+ icon = "✅" if status == "success" else "⚠️"
134
+
135
+ html = f"""
136
+ <div style='background: {bg_color}; padding: 20px; border-radius: 12px; border-left: 5px solid {border_color}; margin: 10px 0;'>
137
+ <div style='display: flex; align-items: center; gap: 15px;'>
138
+ <div style='font-size: 48px;'>{icon}</div>
139
+ <div style='flex: 1;'>
140
+ <h3 style='margin: 0 0 8px 0; color: #1f2937;'>{status_text}</h3>
141
+ <div style='color: #4b5563; font-size: 14px;'>
142
+ <strong>Device:</strong> {device_emoji} {state.device.upper()} |
143
+ <strong>Architecture:</strong> RoBERTa-base |
144
+ <strong>F1 Score:</strong> 0.872
145
+ </div>
146
+ <div style='margin-top: 8px; padding: 8px; background: rgba(255,255,255,0.5); border-radius: 6px; font-size: 13px; color: #374151;'>
147
+ 🎯 Ready to analyze emotions! You can now enter text or try the examples below.
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ """
153
+
154
+ controls_html = _create_controls_panel()
155
+
156
+ print(f"✅ Model ready on {state.device}")
157
+ return html, controls_html
158
+
159
+ except Exception as e:
160
+ state.ready = False
161
+ error_html = f"""
162
+ <div style='background: #fee2e2; padding: 20px; border-radius: 12px; border-left: 5px solid #ef4444;'>
163
+ <div style='display: flex; align-items: center; gap: 15px;'>
164
+ <div style='font-size: 48px;'>❌</div>
165
+ <div>
166
+ <h3 style='margin: 0 0 8px 0; color: #991b1b;'>Error Loading Model</h3>
167
+ <div style='color: #7f1d1d; font-size: 14px; font-family: monospace;'>{str(e)}</div>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ """
172
+ print(f"❌ Error: {e}")
173
+ return error_html, ""
174
+
175
+ # ========== VISUALIZATION FUNCTIONS ==========
176
+ def _create_controls_panel() -> str:
177
+ """Create interactive controls panel"""
178
+ return f"""
179
+ <div style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 15px; border-radius: 10px; color: white; margin: 10px 0;'>
180
+ <div style='display: flex; justify-content: space-around; align-items: center; flex-wrap: wrap; gap: 15px;'>
181
+ <div style='text-align: center;'>
182
+ <div style='font-size: 28px; font-weight: bold;'>{state.predictions_count}</div>
183
+ <div style='font-size: 12px; opacity: 0.9;'>Total Predictions</div>
184
+ </div>
185
+ <div style='text-align: center;'>
186
+ <div style='font-size: 28px; font-weight: bold;'>5</div>
187
+ <div style='font-size: 12px; opacity: 0.9;'>Emotion Classes</div>
188
+ </div>
189
+ <div style='text-align: center;'>
190
+ <div style='font-size: 28px; font-weight: bold;'>0.872</div>
191
+ <div style='font-size: 12px; opacity: 0.9;'>F1 Score</div>
192
+ </div>
193
+ <div style='text-align: center;'>
194
+ <div style='font-size: 28px; font-weight: bold;'>{state.device.upper()}</div>
195
+ <div style='font-size: 12px; opacity: 0.9;'>Device</div>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ """
200
+
201
+ def _create_radar_chart_svg(probs: np.ndarray) -> str:
202
+ """Create SVG radar chart for emotions"""
203
+ size = 400
204
+ padding = 80
205
+ center = size / 2
206
+ max_radius = (size / 2) - padding
207
+
208
+ # Calculate points
209
+ angles = [i * 2 * np.pi / 5 - np.pi/2 for i in range(5)]
210
+ points = []
211
+ for i, prob in enumerate(probs):
212
+ r = prob * max_radius
213
+ x = center + r * np.cos(angles[i])
214
+ y = center + r * np.sin(angles[i])
215
+ points.append(f"{x},{y}")
216
+
217
+ points_str = " ".join(points)
218
+
219
+ # Create axis lines
220
+ axis_lines = ""
221
+ for i in range(5):
222
+ x = center + max_radius * np.cos(angles[i])
223
+ y = center + max_radius * np.sin(angles[i])
224
+ axis_lines += f'<line x1="{center}" y1="{center}" x2="{x}" y2="{y}" stroke="#d1d5db" stroke-width="2"/>'
225
+
226
+ # Create circles (reference lines)
227
+ circles = ""
228
+ for level in [0.25, 0.5, 0.75, 1.0]:
229
+ r = level * max_radius
230
+ circles += f'<circle cx="{center}" cy="{center}" r="{r}" fill="none" stroke="#e5e7eb" stroke-width="1.5" stroke-dasharray="4,4"/>'
231
+
232
+ # Create value points on the data polygon
233
+ data_points = ""
234
+ for i, prob in enumerate(probs):
235
+ r = prob * max_radius
236
+ x = center + r * np.cos(angles[i])
237
+ y = center + r * np.sin(angles[i])
238
+ data_points += f'<circle cx="{x}" cy="{y}" r="6" fill="#667eea" stroke="white" stroke-width="2"/>'
239
+
240
+ # Labels with better positioning
241
+ labels = ""
242
+ for i, emotion in enumerate(EMOTIONS):
243
+ label_radius = max_radius + 30
244
+ x = center + label_radius * np.cos(angles[i])
245
+ y = center + label_radius * np.sin(angles[i])
246
+
247
+ emoji = EMOTION_META[emotion]["emoji"]
248
+ prob_pct = probs[i] * 100
249
+
250
+ # Adjust text anchor based on position
251
+ if x < center - 10:
252
+ anchor = "end"
253
+ elif x > center + 10:
254
+ anchor = "start"
255
+ else:
256
+ anchor = "middle"
257
+
258
+ labels += f'''
259
+ <g>
260
+ <text x="{x}" y="{y - 15}" text-anchor="{anchor}" font-size="28" dominant-baseline="middle">{emoji}</text>
261
+ <text x="{x}" y="{y + 8}" text-anchor="{anchor}" font-size="14" font-weight="600" fill="#1f2937" dominant-baseline="middle">{emotion.capitalize()}</text>
262
+ <text x="{x}" y="{y + 24}" text-anchor="{anchor}" font-size="12" fill="#6b7280" dominant-baseline="middle">{prob_pct:.0f}%</text>
263
+ </g>
264
+ '''
265
+
266
+ return f"""
267
+ <div style="width: 100%; max-width: 450px; margin: 20px auto; padding: 10px; overflow: visible;">
268
+ <svg width="100%" height="100%" viewBox="0 0 {size} {size}" preserveAspectRatio="xMidYMid meet" style="overflow: visible;">
269
+ <defs>
270
+ <filter id="shadow">
271
+ <feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.3"/>
272
+ </filter>
273
+ </defs>
274
+ {circles}
275
+ {axis_lines}
276
+ <polygon points="{points_str}" fill="rgba(102, 126, 234, 0.25)" stroke="#667eea" stroke-width="3" filter="url(#shadow)"/>
277
+ {data_points}
278
+ {labels}
279
+ </svg>
280
+ </div>
281
+ """
282
+
283
+ def _create_emotion_card(emotion: str, prob: float, detected: bool, threshold: float, rank: int) -> str:
284
+ """Create enhanced emotion card with ranking"""
285
+ meta = EMOTION_META[emotion]
286
+ emoji = meta["emoji"]
287
+ color = meta["color"]
288
+ light_color = meta["light_color"]
289
+ gradient = meta["gradient"]
290
+ desc = meta["description"]
291
+
292
+ prob_pct = prob * 100
293
+ intensity, intensity_color = get_intensity_level(prob)
294
+ intensity_label = get_emotion_intensity_label(emotion, prob)
295
+
296
+ # Medal for top 3
297
+ medals = {1: "🥇", 2: "🥈", 3: "🥉"}
298
+ rank_display = medals.get(rank, f"#{rank}")
299
+
300
+ if detected:
301
+ border = f"3px solid {color}"
302
+ shadow = "0 6px 12px rgba(0,0,0,0.15)"
303
+ bg = "white"
304
+ else:
305
+ border = "2px solid #e5e7eb"
306
+ shadow = "0 2px 4px rgba(0,0,0,0.05)"
307
+ bg = "#fafafa"
308
+
309
+ return f"""
310
+ <div style='background: {bg}; padding: 18px; margin: 12px 0; border-radius: 12px; border: {border}; box-shadow: {shadow}; transition: all 0.3s;'>
311
+ <div style='display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;'>
312
+ <div style='display: flex; align-items: center; gap: 12px;'>
313
+ <span style='font-size: 36px;'>{emoji}</span>
314
+ <div>
315
+ <div style='display: flex; align-items: center; gap: 8px;'>
316
+ <span style='font-weight: bold; font-size: 20px; color: #1f2937; text-transform: capitalize;'>{emotion}</span>
317
+ <span style='background: {light_color}; color: {color}; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: bold;'>{rank_display}</span>
318
+ </div>
319
+ <div style='font-size: 13px; color: #6b7280; margin-top: 2px;'>{intensity_label}</div>
320
+ </div>
321
+ </div>
322
+ <div style='text-align: right;'>
323
+ <div style='font-size: 24px; font-weight: bold; color: {color};'>{prob_pct:.1f}%</div>
324
+ <div style='font-size: 11px; color: {intensity_color}; font-weight: 600;'>{intensity}</div>
325
+ </div>
326
+ </div>
327
+
328
+ <div style='position: relative; background: #f3f4f6; height: 28px; border-radius: 14px; overflow: hidden; margin: 10px 0;'>
329
+ <div style='position: absolute; height: 100%; background: {gradient}; width: {min(prob_pct, 100)}%; transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1); border-radius: 14px;'></div>
330
+ <div style='position: absolute; width: 100%; height: 100%; display: flex; align-items: center; padding: 0 12px; justify-content: space-between;'>
331
+ <span style='font-size: 12px; font-weight: 600; color: {"white" if prob_pct > 50 else "#1f2937"};'>{desc}</span>
332
+ <span style='font-size: 11px; color: {"rgba(255,255,255,0.8)" if prob_pct > 50 else "#6b7280"};'>Threshold: {threshold:.1%}</span>
333
+ </div>
334
+ </div>
335
+
336
+ <div style='display: flex; gap: 8px; margin-top: 10px;'>
337
+ <span style='background: {"#10b98120" if detected else "#f3f4f6"}; color: {"#10b981" if detected else "#9ca3af"}; padding: 4px 10px; border-radius: 6px; font-size: 12px; font-weight: 600;'>
338
+ {("✓ DETECTED" if detected else "○ Not Detected")}
339
+ </span>
340
+ <span style='background: #f3f4f6; padding: 4px 10px; border-radius: 6px; font-size: 12px; color: #6b7280;'>
341
+ Confidence: {intensity}
342
+ </span>
343
+ </div>
344
+ </div>
345
+ """
346
+
347
+ def _create_summary_header(probs: np.ndarray, preds: np.ndarray, text: str) -> str:
348
+ """Create comprehensive summary header"""
349
+ detected_count = sum(preds)
350
+ max_idx = np.argmax(probs)
351
+ dominant = EMOTIONS[max_idx]
352
+ dominant_meta = EMOTION_META[dominant]
353
+
354
+ detected_emotions = [EMOTIONS[i] for i in range(len(EMOTIONS)) if preds[i] == 1]
355
+ emotion_emojis = " ".join([EMOTION_META[e]["emoji"] for e in detected_emotions]) if detected_emotions else "😐"
356
+
357
+ word_count = len(text.split())
358
+ char_count = len(text)
359
+ avg_prob = np.mean(probs)
360
+
361
+ # Sentiment description
362
+ if detected_count == 0:
363
+ sentiment = "Neutral/Unclear"
364
+ sentiment_desc = "No strong emotional signals detected"
365
+ sentiment_color = "#9ca3af"
366
+ elif detected_count == 1:
367
+ sentiment = detected_emotions[0].capitalize()
368
+ sentiment_desc = f"Primarily expressing {detected_emotions[0]}"
369
+ sentiment_color = EMOTION_META[detected_emotions[0]]["color"]
370
+ else:
371
+ sentiment = "Mixed Emotions"
372
+ sentiment_desc = f"Complex emotional expression with {detected_count} emotions"
373
+ sentiment_color = "#6366f1"
374
+
375
+ return f"""
376
+ <div style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 16px; color: white; margin: 20px 0; box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);'>
377
+ <div style='text-align: center; margin-bottom: 25px;'>
378
+ <div style='font-size: 56px; margin-bottom: 12px;'>{emotion_emojis}</div>
379
+ <h2 style='margin: 0; font-size: 32px; font-weight: 700;'>{sentiment}</h2>
380
+ <p style='margin: 8px 0 0 0; font-size: 16px; opacity: 0.95;'>{sentiment_desc}</p>
381
+ </div>
382
+
383
+ <div style='display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px;'>
384
+ <div style='background: rgba(255,255,255,0.15); padding: 16px; border-radius: 12px; text-align: center; backdrop-filter: blur(10px);'>
385
+ <div style='font-size: 36px; font-weight: bold;'>{detected_count}</div>
386
+ <div style='font-size: 13px; opacity: 0.95; margin-top: 4px;'>Emotions<br>Detected</div>
387
+ </div>
388
+ <div style='background: rgba(255,255,255,0.15); padding: 16px; border-radius: 12px; text-align: center; backdrop-filter: blur(10px);'>
389
+ <div style='font-size: 36px; font-weight: bold;'>{dominant_meta["emoji"]}</div>
390
+ <div style='font-size: 13px; opacity: 0.95; margin-top: 4px;'>Dominant<br>{dominant.capitalize()}</div>
391
+ </div>
392
+ <div style='background: rgba(255,255,255,0.15); padding: 16px; border-radius: 12px; text-align: center; backdrop-filter: blur(10px);'>
393
+ <div style='font-size: 36px; font-weight: bold;'>{probs[max_idx]:.0%}</div>
394
+ <div style='font-size: 13px; opacity: 0.95; margin-top: 4px;'>Max<br>Confidence</div>
395
+ </div>
396
+ <div style='background: rgba(255,255,255,0.15); padding: 16px; border-radius: 12px; text-align: center; backdrop-filter: blur(10px);'>
397
+ <div style='font-size: 36px; font-weight: bold;'>{avg_prob:.0%}</div>
398
+ <div style='font-size: 13px; opacity: 0.95; margin-top: 4px;'>Average<br>Score</div>
399
+ </div>
400
+ <div style='background: rgba(255,255,255,0.15); padding: 16px; border-radius: 12px; text-align: center; backdrop-filter: blur(10px);'>
401
+ <div style='font-size: 36px; font-weight: bold;'>{word_count}</div>
402
+ <div style='font-size: 13px; opacity: 0.95; margin-top: 4px;'>Words<br>Analyzed</div>
403
+ </div>
404
+ </div>
405
+ </div>
406
+ """
407
+
408
+ def _create_comparison_table(probs: np.ndarray, preds: np.ndarray) -> str:
409
+ """Create comparison table"""
410
+ rows = ""
411
+ for i, emotion in enumerate(EMOTIONS):
412
+ meta = EMOTION_META[emotion]
413
+ detected = "✓" if preds[i] else "○"
414
+ color = meta["color"] if preds[i] else "#9ca3af"
415
+
416
+ rows += f"""
417
+ <tr style='border-bottom: 1px solid #e5e7eb;'>
418
+ <td style='padding: 12px; font-weight: 600; color: {color};'>{meta["emoji"]} {emotion.capitalize()}</td>
419
+ <td style='padding: 12px; text-align: center;'>{probs[i]:.3f}</td>
420
+ <td style='padding: 12px; text-align: center;'>{BEST_THRESHOLDS[i]:.3f}</td>
421
+ <td style='padding: 12px; text-align: center; font-size: 18px; color: {color};'>{detected}</td>
422
+ </tr>
423
+ """
424
+
425
+ return f"""
426
+ <div style='background: white; padding: 20px; border-radius: 12px; margin: 20px 0; border: 1px solid #e5e7eb;'>
427
+ <h3 style='margin: 0 0 15px 0; color: #1f2937;'>📊 Detailed Comparison Table</h3>
428
+ <table style='width: 100%; border-collapse: collapse; font-size: 14px;'>
429
+ <thead>
430
+ <tr style='background: #f9fafb; border-bottom: 2px solid #e5e7eb;'>
431
+ <th style='padding: 12px; text-align: left; color: #6b7280; font-weight: 600;'>Emotion</th>
432
+ <th style='padding: 12px; text-align: center; color: #6b7280; font-weight: 600;'>Probability</th>
433
+ <th style='padding: 12px; text-align: center; color: #6b7280; font-weight: 600;'>Threshold</th>
434
+ <th style='padding: 12px; text-align: center; color: #6b7280; font-weight: 600;'>Detected</th>
435
+ </tr>
436
+ </thead>
437
+ <tbody>
438
+ {rows}
439
+ </tbody>
440
+ </table>
441
+ </div>
442
+ """
443
+
444
+ # ========== PREDICTION FUNCTIONS ==========
445
+ def predict_emotion(text: str, show_radar: bool = True, show_table: bool = True) -> Tuple[str, str, str, str, str]:
446
+ """Main prediction function with all outputs"""
447
+ if not state.ready or state.model is None or state.tokenizer is None:
448
+ error = """
449
+ <div style='padding: 40px; text-align: center; background: #fef2f2; border: 3px solid #fca5a5; border-radius: 16px;'>
450
+ <div style='font-size: 64px; margin-bottom: 15px;'>⚠️</div>
451
+ <h2 style='color: #991b1b; margin: 10px 0;'>Model Not Loaded</h2>
452
+ <p style='color: #7f1d1d; font-size: 16px;'>Please click <strong>'🚀 Load Model'</strong> button to initialize.</p>
453
+ </div>
454
+ """
455
+ return error, "", "", "{}", _create_controls_panel()
456
+
457
+ if not text or not text.strip():
458
+ error = """
459
+ <div style='padding: 40px; text-align: center; background: #fefce8; border: 3px solid #fde047; border-radius: 16px;'>
460
+ <div style='font-size: 64px; margin-bottom: 15px;'>✍️</div>
461
+ <h2 style='color: #854d0e; margin: 10px 0;'>No Text Provided</h2>
462
+ <p style='color: #713f12; font-size: 16px;'>Enter text above or try one of the examples below.</p>
463
+ </div>
464
+ """
465
+ return error, "", "", "{}", _create_controls_panel()
466
+
467
+ try:
468
+ # Tokenize and predict
469
+ encoding = state.tokenizer(
470
+ text.strip(),
471
+ truncation=True,
472
+ padding="max_length",
473
+ max_length=MAX_LEN,
474
+ return_tensors="pt"
475
+ )
476
+
477
+ with torch.no_grad():
478
+ logits = state.model(
479
+ encoding["input_ids"].to(state.device),
480
+ encoding["attention_mask"].to(state.device)
481
+ )
482
+ probs = sigmoid(logits.cpu().numpy())[0]
483
+ preds = (probs > np.array(BEST_THRESHOLDS)).astype(int)
484
+
485
+ # Update stats
486
+ state.predictions_count += 1
487
+ for i, emo in enumerate(EMOTIONS):
488
+ if preds[i]: state.emotion_stats[emo] += 1
489
+
490
+ state.history.append({
491
+ "text": text[:100],
492
+ "timestamp": datetime.now().isoformat(),
493
+ "detected": sum(preds)
494
+ })
495
+
496
+ # Create visualizations
497
+ summary_html = _create_summary_header(probs, preds, text)
498
+
499
+ # Radar chart
500
+ radar_html = ""
501
+ if show_radar:
502
+ radar_html = f"""
503
+ <div style='background: white; padding: 20px; border-radius: 12px; margin: 20px 0; text-align: center; border: 1px solid #e5e7eb;'>
504
+ <h3 style='margin: 0 0 10px 0; color: #1f2937;'>📡 Emotion Radar Chart</h3>
505
+ {_create_radar_chart_svg(probs)}
506
+ <p style='color: #6b7280; font-size: 13px; margin: 10px 0 0 0;'>Visual representation of emotion intensities</p>
507
+ </div>
508
+ """
509
+
510
+ # Emotion cards with ranking
511
+ ranked_indices = np.argsort(probs)[::-1]
512
+ cards_html = "<div style='background: #f9fafb; padding: 20px; border-radius: 12px;'>"
513
+ cards_html += "<h3 style='color: #1f2937; margin: 0 0 15px 0; font-size: 22px;'>🎯 Detailed Emotion Analysis</h3>"
514
+ for rank, idx in enumerate(ranked_indices, 1):
515
+ cards_html += _create_emotion_card(EMOTIONS[idx], probs[idx], preds[idx] == 1, BEST_THRESHOLDS[idx], rank)
516
+ cards_html += "</div>"
517
+
518
+ # Comparison table
519
+ table_html = _create_comparison_table(probs, preds) if show_table else ""
520
+
521
+ # JSON output
522
+ json_results = {
523
+ "metadata": {
524
+ "timestamp": datetime.now().isoformat(),
525
+ "text_length": len(text),
526
+ "word_count": len(text.split()),
527
+ "model": "roberta-base",
528
+ "prediction_id": state.predictions_count
529
+ },
530
+ "emotions": {
531
+ emo: {
532
+ "probability": round(float(probs[i]), 4),
533
+ "detected": bool(preds[i]),
534
+ "threshold": float(BEST_THRESHOLDS[i]),
535
+ "confidence_level": get_intensity_level(probs[i])[0],
536
+ "rank": int(np.where(ranked_indices == i)[0][0] + 1),
537
+ "intensity_label": get_emotion_intensity_label(emo, probs[i])
538
+ }
539
+ for i, emo in enumerate(EMOTIONS)
540
+ },
541
+ "summary": {
542
+ "total_detected": int(sum(preds)),
543
+ "dominant_emotion": EMOTIONS[np.argmax(probs)],
544
+ "dominant_probability": round(float(probs[np.argmax(probs)]), 4),
545
+ "average_probability": round(float(np.mean(probs)), 4),
546
+ "detected_emotions": [EMOTIONS[i] for i in range(len(EMOTIONS)) if preds[i] == 1]
547
+ }
548
+ }
549
+
550
+ # Updated controls
551
+ controls_html = _create_controls_panel()
552
+ # Updated controls
553
+ controls_html = _create_controls_panel()
554
+
555
+ return summary_html, radar_html + cards_html, table_html, json.dumps(json_results, indent=2), controls_html
556
+
557
+ except Exception as e:
558
+ error = f"""
559
+ <div style='padding: 40px; text-align: center; background: #fee; border: 3px solid #fca5a5; border-radius: 16px;'>
560
+ <div style='font-size: 64px; margin-bottom: 15px;'>❌</div>
561
+ <h2 style='color: #991b1b; margin: 10px 0;'>Prediction Error</h2>
562
+ <pre style='color: #7f1d1d; text-align: left; padding: 15px; background: #fef2f2; border-radius: 8px; overflow-x: auto;'>{str(e)}</pre>
563
+ </div>
564
+ """
565
+ return error, "", "", "{}", _create_controls_panel()
566
+
567
+ def batch_predict(texts: str) -> str:
568
+ """Batch prediction with rich output"""
569
+ if not state.ready:
570
+ return "<div style='padding: 20px; background: #fee2e2; border-radius: 8px;'>⚠️ Model not loaded</div>"
571
+
572
+ lines = [line.strip() for line in texts.split('\n') if line.strip()]
573
+ if not lines:
574
+ return "<div style='padding: 20px; background: #fef3c7; border-radius: 8px;'>⚠️ No text provided</div>"
575
+
576
+ html = f"""
577
+ <div style='font-family: system-ui, sans-serif;'>
578
+ <div style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 12px; color: white; margin-bottom: 20px;'>
579
+ <h2 style='margin: 0; font-size: 24px;'>📊 Batch Analysis Results</h2>
580
+ <p style='margin: 8px 0 0 0; opacity: 0.9;'>Analyzed {len(lines)} text samples</p>
581
+ </div>
582
+ """
583
+
584
+ all_emotions = defaultdict(int)
585
+
586
+ for i, text in enumerate(lines, 1):
587
+ summary, _, _, json_data, _ = predict_emotion(text, show_radar=False, show_table=False)
588
+ try:
589
+ data = json.loads(json_data)
590
+ detected_count = data['summary']['total_detected']
591
+ dominant = data['summary']['dominant_emotion']
592
+ dominant_prob = data['summary']['dominant_probability']
593
+ detected_list = data['summary']['detected_emotions']
594
+
595
+ for emo in detected_list:
596
+ all_emotions[emo] += 1
597
+
598
+ emoji = EMOTION_META[dominant]['emoji']
599
+ color = EMOTION_META[dominant]['color']
600
+
601
+ html += f"""
602
+ <div style='background: white; padding: 18px; margin: 12px 0; border-radius: 10px; border-left: 5px solid {color}; box-shadow: 0 2px 8px rgba(0,0,0,0.1);'>
603
+ <div style='display: flex; justify-content: between; align-items: start; gap: 15px; margin-bottom: 10px;'>
604
+ <div style='font-size: 36px;'>{emoji}</div>
605
+ <div style='flex: 1;'>
606
+ <div style='font-weight: bold; color: #1f2937; font-size: 16px; margin-bottom: 5px;'>Sample #{i}</div>
607
+ <div style='color: #4b5563; font-style: italic; margin-bottom: 10px; line-height: 1.5;'>"{text[:120]}{'...' if len(text) > 120 else ''}"</div>
608
+ <div style='display: flex; gap: 12px; flex-wrap: wrap; font-size: 14px;'>
609
+ <span style='background: #f3f4f6; padding: 6px 12px; border-radius: 6px; color: #374151;'>
610
+ <strong>Dominant:</strong> {emoji} {dominant.capitalize()}
611
+ </span>
612
+ <span style='background: #f3f4f6; padding: 6px 12px; border-radius: 6px; color: #374151;'>
613
+ <strong>Confidence:</strong> {dominant_prob:.1%}
614
+ </span>
615
+ <span style='background: #f3f4f6; padding: 6px 12px; border-radius: 6px; color: #374151;'>
616
+ <strong>Total:</strong> {detected_count}/5
617
+ </span>
618
+ </div>
619
+ {f"<div style='margin-top: 8px; font-size: 13px; color: #6b7280;'><strong>Detected:</strong> {', '.join([e.capitalize() for e in detected_list])}</div>" if detected_list else ""}
620
+ </div>
621
+ </div>
622
+ </div>
623
+ """
624
+ except Exception as e:
625
+ html += f"<div style='background: #fee; padding: 15px; margin: 10px 0; border-radius: 8px;'>Sample {i}: Error - {str(e)}</div>"
626
+
627
+ # Summary statistics
628
+ html += """
629
+ <div style='background: #f9fafb; padding: 20px; border-radius: 12px; margin-top: 20px; border: 2px solid #e5e7eb;'>
630
+ <h3 style='margin: 0 0 15px 0; color: #1f2937;'>📈 Aggregate Statistics</h3>
631
+ <div style='display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px;'>
632
+ """
633
+
634
+ for emotion in EMOTIONS:
635
+ count = all_emotions[emotion]
636
+ pct = (count / len(lines)) * 100 if lines else 0
637
+ meta = EMOTION_META[emotion]
638
+ html += f"""
639
+ <div style='background: white; padding: 15px; border-radius: 8px; text-align: center; border: 2px solid {meta["light_color"]}'>
640
+ <div style='font-size: 32px; margin-bottom: 5px;'>{meta["emoji"]}</div>
641
+ <div style='font-weight: bold; color: {meta["color"]}; font-size: 20px;'>{count}</div>
642
+ <div style='font-size: 12px; color: #6b7280; margin-top: 3px;'>{emotion.capitalize()}</div>
643
+ <div style='font-size: 11px; color: #9ca3af;'>{pct:.0f}% of samples</div>
644
+ </div>
645
+ """
646
+
647
+ html += "</div></div></div>"
648
+ return html
649
+
650
+ def get_history() -> str:
651
+ """Get prediction history"""
652
+ if not state.history:
653
+ return "<div style='padding: 20px; text-align: center; color: #9ca3af;'>No predictions yet</div>"
654
+
655
+ html = f"""
656
+ <div style='background: white; padding: 20px; border-radius: 12px; border: 1px solid #e5e7eb;'>
657
+ <h3 style='margin: 0 0 15px 0; color: #1f2937;'>📜 Recent Predictions ({len(state.history)})</h3>
658
+ """
659
+
660
+ for i, record in enumerate(reversed(state.history[-10:]), 1):
661
+ time = datetime.fromisoformat(record['timestamp']).strftime('%H:%M:%S')
662
+ html += f"""
663
+ <div style='padding: 12px; margin: 8px 0; background: #f9fafb; border-radius: 8px; border-left: 3px solid #667eea;'>
664
+ <div style='display: flex; justify-content: space-between; align-items: center;'>
665
+ <div style='flex: 1;'>
666
+ <div style='font-size: 13px; color: #6b7280; margin-bottom: 4px;'>{time}</div>
667
+ <div style='color: #374151; font-size: 14px;'>"{record['text']}{'...' if len(record['text']) >= 100 else ''}"</div>
668
+ </div>
669
+ <div style='background: #667eea; color: white; padding: 4px 12px; border-radius: 12px; font-size: 13px; font-weight: bold;'>
670
+ {record['detected']}/5
671
+ </div>
672
+ </div>
673
+ </div>
674
+ """
675
+
676
+ html += "</div>"
677
+ return html
678
+
679
+ def export_results(json_str: str) -> str:
680
+ """Export results as downloadable JSON"""
681
+ return json_str
682
+
683
+ # ========== GRADIO UI ==========
684
+ def create_interface():
685
+ with gr.Blocks(title="🎭 Emotion Classifier") as demo:
686
+
687
+ gr.HTML("""
688
+ <div style='text-align: center; padding: 40px 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 20px; margin-bottom: 25px; color: white; box-shadow: 0 10px 40px rgba(102, 126, 234, 0.3);'>
689
+ <div style='font-size: 72px; margin-bottom: 10px;'>🎭</div>
690
+ <h1 style='font-size: 48px; margin: 0; font-weight: 800; letter-spacing: -1px;'>Emotion Classifier</h1>
691
+ <p style='font-size: 20px; margin: 15px 0 8px 0; opacity: 0.95; font-weight: 500;'>Advanced Multi-Label Emotion Detection</p>
692
+ <p style='font-size: 15px; margin: 0; opacity: 0.85;'>Powered by RoBERTa-base • F1 Score: 0.872 • 5 Emotion Classes</p>
693
+ <div style='margin-top: 20px; display: flex; justify-content: center; gap: 15px; flex-wrap: wrap;'>
694
+ <span style='background: rgba(255,255,255,0.2); padding: 8px 16px; border-radius: 20px; font-size: 14px; backdrop-filter: blur(10px);'>😠 Anger</span>
695
+ <span style='background: rgba(255,255,255,0.2); padding: 8px 16px; border-radius: 20px; font-size: 14px; backdrop-filter: blur(10px);'>😨 Fear</span>
696
+ <span style='background: rgba(255,255,255,0.2); padding: 8px 16px; border-radius: 20px; font-size: 14px; backdrop-filter: blur(10px);'>😊 Joy</span>
697
+ <span style='background: rgba(255,255,255,0.2); padding: 8px 16px; border-radius: 20px; font-size: 14px; backdrop-filter: blur(10px);'>😢 Sadness</span>
698
+ <span style='background: rgba(255,255,255,0.2); padding: 8px 16px; border-radius: 20px; font-size: 14px; backdrop-filter: blur(10px);'>😲 Surprise</span>
699
+ </div>
700
+ </div>
701
+ """)
702
+
703
+ # Status and Controls
704
+ with gr.Row():
705
+ with gr.Column(scale=3):
706
+ status_display = gr.HTML("""
707
+ <div style='background: #fef3c7; padding: 20px; border-radius: 12px; border-left: 5px solid #f59e0b;'>
708
+ <h3 style='margin: 0 0 8px 0; color: #92400e;'>⏳ Model Status: Not Loaded</h3>
709
+ <p style='margin: 0; color: #78350f;'>Click the <strong>'🚀 Load Model'</strong> button to initialize the classifier.</p>
710
+ </div>
711
+ """)
712
+ with gr.Column(scale=1):
713
+ load_btn = gr.Button("🚀 Load Model", variant="primary", size="lg")
714
+
715
+ controls_panel = gr.HTML("")
716
+
717
+ gr.HTML("<hr style='margin: 30px 0; border: none; border-top: 3px solid #e5e7eb;'>")
718
+
719
+ # Main Tabs
720
+ with gr.Tabs():
721
+ with gr.Tab("📝 Single Analysis"):
722
+ with gr.Row():
723
+ with gr.Column(scale=1):
724
+ input_text = gr.Textbox(
725
+ label="💬 Enter Text to Analyze",
726
+ placeholder="Type your text here... \n\nExample: I'm incredibly excited about this opportunity, but I'm also feeling a bit nervous about the upcoming challenges!",
727
+ lines=10,
728
+ max_lines=20
729
+ )
730
+
731
+ with gr.Row():
732
+ analyze_btn = gr.Button("🔍 Analyze Emotions", variant="primary", size="lg")
733
+ clear_input_btn = gr.ClearButton([input_text], value="🗑️ Clear Input", size="lg")
734
+
735
+ with gr.Row():
736
+ show_radar_check = gr.Checkbox(label="Show Radar Chart", value=True)
737
+ show_table_check = gr.Checkbox(label="Show Comparison Table", value=True)
738
+
739
+ gr.Markdown("### 💡 Try These Example Texts")
740
+ gr.Examples(
741
+ examples=[
742
+ ["I'm absolutely thrilled and overjoyed! This is the best news I've received in years!"],
743
+ ["This is completely unacceptable! I'm furious about how this situation was handled!"],
744
+ ["I'm terrified about the future. The uncertainty is overwhelming and I can't stop worrying."],
745
+ ["Oh my god! I cannot believe this just happened! This is absolutely shocking!"],
746
+ ["I feel so empty and sad. Nothing seems to matter anymore and I just want to cry."],
747
+ ["I'm excited about the new job, but I'm also really anxious about moving to a new city."],
748
+ ["The concert was amazing! But now I'm sad it's over and I'll miss my friends."],
749
+ ["I'm angry at myself for making such a stupid mistake. I should have known better!"],
750
+ ["What a wonderful surprise! I never expected this and I'm so grateful and happy!"],
751
+ ["This project terrifies me. I'm worried I'll fail, but I'm also determined to succeed."]
752
+ ],
753
+ inputs=[input_text],
754
+ label="Click to try"
755
+ )
756
+
757
+ with gr.Column(scale=1):
758
+ gr.Markdown("### 📊 Analysis Results")
759
+
760
+ clear_results_btn = gr.Button("🗑️ Clear Results", variant="secondary", size="sm")
761
+
762
+ summary_html = gr.HTML(label="Summary")
763
+ details_html = gr.HTML(label="Detailed Analysis")
764
+ table_html = gr.HTML(label="Comparison")
765
+
766
+ with gr.Tab("📊 Batch Analysis"):
767
+ gr.Markdown("""
768
+ ### Analyze Multiple Texts Simultaneously
769
+ Enter one text per line to analyze multiple samples at once. Perfect for:
770
+ - Customer feedback analysis
771
+ - Social media monitoring
772
+ - Survey responses
773
+ - Product reviews
774
+ """)
775
+
776
+ batch_input = gr.Textbox(
777
+ label="Enter Multiple Texts (one per line)",
778
+ placeholder="I love this product!\nThis service is terrible.\nWhat an amazing surprise!\nFeeling very sad today.",
779
+ lines=12
780
+ )
781
+
782
+ batch_btn = gr.Button("🔍 Analyze All Texts", variant="primary", size="lg")
783
+ batch_output = gr.HTML(label="Batch Results")
784
+
785
+ with gr.Tab("💾 JSON Export"):
786
+ gr.Markdown("""
787
+ ### Developer-Friendly JSON Output
788
+ Get structured data for integration with your applications, APIs, or data pipelines.
789
+ """)
790
+
791
+ json_output = gr.Code(
792
+ label="JSON Results (automatically updated)",
793
+ language="json",
794
+ lines=25
795
+ )
796
+
797
+ gr.Markdown("""
798
+ **JSON Structure includes:**
799
+ - Metadata (timestamp, text stats, model info)
800
+ - Individual emotion scores with confidence levels
801
+ - Rankings and intensity labels
802
+ - Summary statistics
803
+ - Detected emotions list
804
+ """)
805
+
806
+ with gr.Tab("📜 History"):
807
+ gr.Markdown("### Recent Predictions")
808
+ history_btn = gr.Button("🔄 Refresh History", variant="secondary")
809
+ history_output = gr.HTML()
810
+
811
+ # Footer Info
812
+ gr.HTML("""
813
+ <div style='margin-top: 40px; padding: 30px; background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%); border-radius: 16px; border: 2px solid #e5e7eb;'>
814
+ <h2 style='color: #1f2937; margin-top: 0; font-size: 24px;'>📚 Model Documentation</h2>
815
+
816
+ <div style='display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin: 20px 0;'>
817
+ <div style='background: white; padding: 20px; border-radius: 12px; border-left: 4px solid #667eea;'>
818
+ <h3 style='margin: 0 0 10px 0; color: #667eea;'>🏗️ Architecture</h3>
819
+ <ul style='margin: 0; padding-left: 20px; color: #4b5563; line-height: 1.8;'>
820
+ <li><strong>Base Model:</strong> RoBERTa-base</li>
821
+ <li><strong>Parameters:</strong> 125M</li>
822
+ <li><strong>Classifier:</strong> Custom head with dropout</li>
823
+ <li><strong>Training:</strong> Fine-tuned on emotion corpus</li>
824
+ </ul>
825
+ </div>
826
+
827
+ <div style='background: white; padding: 20px; border-radius: 12px; border-left: 4px solid #10b981;'>
828
+ <h3 style='margin: 0 0 10px 0; color: #10b981;'>📊 Performance</h3>
829
+ <ul style='margin: 0; padding-left: 20px; color: #4b5563; line-height: 1.8;'>
830
+ <li><strong>F1 Score:</strong> 0.872 (validation)</li>
831
+ <li><strong>Task:</strong> Multi-label classification</li>
832
+ <li><strong>Classes:</strong> 5 emotions</li>
833
+ <li><strong>Thresholds:</strong> Individually optimized</li>
834
+ </ul>
835
+ </div>
836
+
837
+ <div style='background: white; padding: 20px; border-radius: 12px; border-left: 4px solid #f59e0b;'>
838
+ <h3 style='margin: 0 0 10px 0; color: #f59e0b;'>🎯 Capabilities</h3>
839
+ <ul style='margin: 0; padding-left: 20px; color: #4b5563; line-height: 1.8;'>
840
+ <li><strong>Multi-label:</strong> Detect multiple emotions</li>
841
+ <li><strong>Intensity:</strong> Confidence scoring</li>
842
+ <li><strong>Speed:</strong> Real-time inference</li>
843
+ <li><strong>Batch:</strong> Analyze multiple texts</li>
844
+ </ul>
845
+ </div>
846
+ </div>
847
+
848
+ <div style='background: white; padding: 20px; border-radius: 12px; margin-top: 20px;'>
849
+ <h3 style='margin: 0 0 15px 0; color: #1f2937;'>🔬 How It Works</h3>
850
+ <div style='color: #4b5563; line-height: 1.8;'>
851
+ <ol style='margin: 0; padding-left: 20px;'>
852
+ <li><strong>Tokenization:</strong> Text is converted to subword tokens using RoBERTa's byte-pair encoding</li>
853
+ <li><strong>Encoding:</strong> Tokens are embedded and processed through 12 transformer layers</li>
854
+ <li><strong>Classification:</strong> Pooled representation passes through dropout and linear layer</li>
855
+ <li><strong>Thresholding:</strong> Sigmoid probabilities compared against optimized per-class thresholds</li>
856
+ <li><strong>Output:</strong> Binary predictions for each emotion with confidence scores</li>
857
+ </ol>
858
+ </div>
859
+ </div>
860
+
861
+ <div style='margin-top: 20px; padding: 15px; background: #eff6ff; border-radius: 8px; border-left: 4px solid #3b82f6;'>
862
+ <strong style='color: #1e40af;'>💡 Tip:</strong>
863
+ <span style='color: #1e3a8a;'>The model performs best on clear, expressive text. Longer texts (20+ words) generally yield more accurate results.</span>
864
+ </div>
865
+ </div>
866
+ """)
867
+
868
+ # Event Handlers
869
+ load_btn.click(
870
+ fn=load_model,
871
+ outputs=[status_display, controls_panel]
872
+ )
873
+
874
+ analyze_btn.click(
875
+ fn=predict_emotion,
876
+ inputs=[input_text, show_radar_check, show_table_check],
877
+ outputs=[summary_html, details_html, table_html, json_output, controls_panel]
878
+ )
879
+
880
+ input_text.submit(
881
+ fn=predict_emotion,
882
+ inputs=[input_text, show_radar_check, show_table_check],
883
+ outputs=[summary_html, details_html, table_html, json_output, controls_panel]
884
+ )
885
+
886
+ clear_results_btn.click(
887
+ fn=lambda: ("", "", "", "{}", _create_controls_panel()),
888
+ outputs=[summary_html, details_html, table_html, json_output, controls_panel]
889
+ )
890
+
891
+ batch_btn.click(
892
+ fn=batch_predict,
893
+ inputs=[batch_input],
894
+ outputs=[batch_output]
895
+ )
896
+
897
+ history_btn.click(
898
+ fn=get_history,
899
+ outputs=[history_output]
900
+ )
901
+
902
+ return demo
903
+
904
+ # ========== MAIN ==========
905
+ if __name__ == "__main__":
906
+ print("🎭 Starting Gradio interface...")
907
+ demo = create_interface()
908
+ demo.launch(
909
+ server_name="0.0.0.0",
910
+ server_port=7860,
911
+ share=False,
912
+ show_error=True
913
+ )