hmusman2804045-max commited on
Commit
b31728c
·
0 Parent(s):

Phase 1: Environment setup and training pipeline

Browse files
.gitignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ models/
2
+ data/
3
+ logs/
4
+ __pycache__/
5
+ urdu_env/
6
+ venv/
7
+ *.docx
8
+ *.html
9
+ *.pdf
10
+ .env
11
+ remove_comments_script.py
12
+ *.tsv
Dockerfile ADDED
File without changes
README.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Urdu Sentiment and Emotion Analysis Engine
2
+
3
+ Welcome to the Urdu Sentiment and Emotion Analysis Engine project! This repository contains the code for a multilingual NLP system that classifies sentiment (Positive, Negative, Neutral) and emotion (Joy, Anger, Fear, Sadness) from Urdu, Roman Urdu, and mixed-language text using a fine-tuned XLM-RoBERTa transformer.
4
+
5
+ ## Phase 1: Environment Setup & Training Pipeline
6
+ This initial commit includes **Phase 1** of our project roadmap: setting up the environment, establishing the dataset loaders, and building the initial training scripts for the sentiment and emotion models.
7
+
8
+ ### Repository Structure (Phase 1)
9
+ - `requirements.txt`: Environment dependencies required for training and inference.
10
+ - `training/`: Contains the core scripts for data processing and model fine-tuning.
11
+ - `dataset.py`: PyTorch `Dataset` implementation for loading and tokenizing text using XLM-RoBERTa.
12
+ - `train_sentiment.py`: Training script for the sentiment classification model.
13
+ - `train_emotion.py`: Training script for the emotion classification model.
14
+ - `test_models.py`: A utility script to load trained models and run interactive inference.
15
+
16
+ ### Environment Setup
17
+ To get started, create a virtual environment and install the required dependencies:
18
+
19
+ ```bash
20
+ # Create a virtual environment
21
+ python -m venv urdu_env
22
+
23
+ # Activate the virtual environment
24
+ # On Windows:
25
+ urdu_env\Scripts\activate
26
+ # On Linux/Mac:
27
+ source urdu_env/bin/activate
28
+
29
+ # Install dependencies
30
+ pip install -r requirements.txt
31
+ ```
32
+
33
+ ### Next Steps
34
+ The models are trained using HuggingFace's Trainer API on local datasets (which have been kept separate from the repository due to size constraints). In upcoming phases, we will introduce the Flask Backend (REST API), Frontend Dashboard, and cloud deployment via HuggingFace Spaces.
35
+
36
+ Stay tuned for Phase 2 updates!
app.py ADDED
File without changes
lang_detector.py ADDED
File without changes
predictor.py ADDED
File without changes
requirements.txt ADDED
File without changes
static/script.js ADDED
@@ -0,0 +1,334 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ // Theme toggling
3
+ const themeToggle = document.getElementById('themeToggle');
4
+ const body = document.body;
5
+
6
+ themeToggle.addEventListener('click', () => {
7
+ body.classList.toggle('light-theme');
8
+ // Update charts on theme change
9
+ updateChartColors();
10
+ });
11
+
12
+ // Auto-resize textarea
13
+ const textInput = document.getElementById('textInput');
14
+ textInput.addEventListener('input', function() {
15
+ this.style.height = 'auto';
16
+ this.style.height = (this.scrollHeight) + 'px';
17
+
18
+ // Auto-detect direction (simple heuristic)
19
+ const urduRegex = /[\u0600-\u06FF]/;
20
+ if (urduRegex.test(this.value)) {
21
+ this.style.direction = 'rtl';
22
+ this.classList.add('urdu-text');
23
+ } else {
24
+ this.style.direction = 'ltr';
25
+ this.classList.remove('urdu-text');
26
+ }
27
+ });
28
+
29
+ // Handle Analysis
30
+ const analyzeBtn = document.getElementById('analyzeBtn');
31
+ const resultsCard = document.getElementById('resultsCard');
32
+
33
+ analyzeBtn.addEventListener('click', async () => {
34
+ const text = textInput.value.trim();
35
+ if (!text) return;
36
+
37
+ // UI Loading state
38
+ const originalBtnContent = analyzeBtn.innerHTML;
39
+ analyzeBtn.innerHTML = `<span>Analyzing...</span><svg class="spinner" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:20px;height:20px;"><path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>`;
40
+ analyzeBtn.disabled = true;
41
+
42
+ try {
43
+ // Attempt to hit real backend
44
+ let data;
45
+ try {
46
+ const response = await fetch('/analyze', {
47
+ method: 'POST',
48
+ headers: { 'Content-Type': 'application/json' },
49
+ body: JSON.stringify({ text })
50
+ });
51
+ if (response.ok) {
52
+ data = await response.json();
53
+ } else {
54
+ throw new Error('Backend not ready');
55
+ }
56
+ } catch (err) {
57
+ console.log("Mocking response since backend isn't available:", err);
58
+ // Mock fallback for UI demonstration
59
+ await new Promise(r => setTimeout(r, 1200)); // Simulate delay
60
+ data = generateMockResult(text);
61
+ }
62
+
63
+ displayResults(data);
64
+ updateAnalyticsMock();
65
+ } catch (error) {
66
+ console.error('Analysis error:', error);
67
+ alert('Error analyzing text. Please try again.');
68
+ } finally {
69
+ analyzeBtn.innerHTML = originalBtnContent;
70
+ analyzeBtn.disabled = false;
71
+ }
72
+ });
73
+
74
+ // Charts Initialization
75
+ initCharts();
76
+
77
+ // Start Live Feed Simulation
78
+ startLiveFeed();
79
+
80
+ // Init Keywords
81
+ initKeywords();
82
+ });
83
+
84
+ // Mock Data Generator
85
+ function generateMockResult(text) {
86
+ const isUrdu = /[\u0600-\u06FF]/.test(text);
87
+ const lang = isUrdu ? 'Urdu' : 'Roman Urdu / English';
88
+
89
+ // Randomize for demo
90
+ const sentiments = ['Positive', 'Negative', 'Neutral'];
91
+ const emotions = ['Joy', 'Anger', 'Fear', 'Sadness'];
92
+
93
+ const sentiment = sentiments[Math.floor(Math.random() * sentiments.length)];
94
+ const emotion = emotions[Math.floor(Math.random() * emotions.length)];
95
+ const confS = (Math.random() * 20 + 75).toFixed(1); // 75-95%
96
+ const confE = (Math.random() * 30 + 60).toFixed(1); // 60-90%
97
+
98
+ // Attention map mock
99
+ const words = text.split(/\s+/);
100
+ const attentionWords = words.map(word => {
101
+ return {
102
+ word: word,
103
+ weight: Math.random() // 0 to 1
104
+ };
105
+ });
106
+
107
+ return {
108
+ language: lang,
109
+ sentiment: { label: sentiment, confidence: parseFloat(confS) },
110
+ emotion: { label: emotion, confidence: parseFloat(confE) },
111
+ attention: attentionWords
112
+ };
113
+ }
114
+
115
+ // Display Results
116
+ function displayResults(data) {
117
+ document.getElementById('detectedLang').textContent = data.language;
118
+ document.getElementById('resultsCard').style.display = 'block';
119
+
120
+ // Update Sentiment
121
+ const sBox = document.getElementById('sentimentResult');
122
+ const sConfBar = document.getElementById('sentimentConfidence');
123
+ const sConfText = document.getElementById('sentimentConfText');
124
+
125
+ let sEmoji = '😐', sColor = 'var(--neutral)';
126
+ if (data.sentiment.label === 'Positive') { sEmoji = '😊'; sColor = 'var(--positive)'; }
127
+ if (data.sentiment.label === 'Negative') { sEmoji = '😠'; sColor = 'var(--negative)'; }
128
+
129
+ sBox.innerHTML = `<span class="emoji">${sEmoji}</span><span class="label" style="color: ${sColor}">${data.sentiment.label}</span>`;
130
+ sConfBar.style.width = '0%'; // Reset for animation
131
+ sConfBar.style.backgroundColor = sColor;
132
+ setTimeout(() => {
133
+ sConfBar.style.width = `${data.sentiment.confidence}%`;
134
+ }, 100);
135
+ sConfText.textContent = `${data.sentiment.confidence}% Confidence`;
136
+
137
+ // Update Emotion
138
+ const eBox = document.getElementById('emotionResult');
139
+ const eConfBar = document.getElementById('emotionConfidence');
140
+ const eConfText = document.getElementById('emotionConfText');
141
+
142
+ let eEmoji = '🤔', eColor = 'var(--primary)';
143
+ if (data.emotion.label === 'Joy') { eEmoji = '😄'; eColor = 'var(--joy)'; }
144
+ if (data.emotion.label === 'Anger') { eEmoji = '😡'; eColor = 'var(--anger)'; }
145
+ if (data.emotion.label === 'Fear') { eEmoji = '😨'; eColor = 'var(--fear)'; }
146
+ if (data.emotion.label === 'Sadness') { eEmoji = '😢'; eColor = 'var(--sadness)'; }
147
+
148
+ eBox.innerHTML = `<span class="emoji">${eEmoji}</span><span class="label" style="color: ${eColor}">${data.emotion.label}</span>`;
149
+ eConfBar.style.width = '0%'; // Reset for animation
150
+ eConfBar.style.backgroundColor = eColor;
151
+ setTimeout(() => {
152
+ eConfBar.style.width = `${data.emotion.confidence}%`;
153
+ }, 100);
154
+ eConfText.textContent = `${data.emotion.confidence}% Confidence`;
155
+
156
+ // Render Attention Map
157
+ const attContainer = document.getElementById('attentionResult');
158
+ attContainer.innerHTML = '';
159
+
160
+ const isRtl = /[\u0600-\u06FF]/.test(data.attention.map(w=>w.word).join(' '));
161
+ attContainer.style.direction = isRtl ? 'rtl' : 'ltr';
162
+
163
+ data.attention.forEach(item => {
164
+ const span = document.createElement('span');
165
+ span.textContent = item.word + ' ';
166
+ span.className = 'attention-word';
167
+
168
+ span.style.backgroundColor = `rgba(99, 102, 241, ${item.weight * 0.5})`;
169
+ span.title = `Attention Weight: ${(item.weight).toFixed(2)}`;
170
+
171
+ attContainer.appendChild(span);
172
+ });
173
+
174
+ // Scroll to results
175
+ setTimeout(() => {
176
+ document.getElementById('resultsCard').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
177
+ }, 100);
178
+ }
179
+
180
+ // Charts
181
+ let sentimentChart, emotionChart;
182
+
183
+ function initCharts() {
184
+ const textColor = getComputedStyle(document.body).getPropertyValue('--text-primary').trim() || '#f9fafb';
185
+
186
+ // Sentiment Donut
187
+ const ctxS = document.getElementById('sentimentChart').getContext('2d');
188
+ sentimentChart = new Chart(ctxS, {
189
+ type: 'doughnut',
190
+ data: {
191
+ labels: ['Positive', 'Negative', 'Neutral'],
192
+ datasets: [{
193
+ data: [45, 30, 25], // initial mock data
194
+ backgroundColor: ['#10b981', '#ef4444', '#6b7280'],
195
+ borderWidth: 0,
196
+ hoverOffset: 4
197
+ }]
198
+ },
199
+ options: {
200
+ responsive: true,
201
+ maintainAspectRatio: false,
202
+ plugins: {
203
+ legend: { position: 'bottom', labels: { color: textColor } },
204
+ title: { display: true, text: 'Sentiment Distribution', color: textColor }
205
+ },
206
+ cutout: '70%'
207
+ }
208
+ });
209
+
210
+ // Emotion Bar
211
+ const ctxE = document.getElementById('emotionChart').getContext('2d');
212
+ emotionChart = new Chart(ctxE, {
213
+ type: 'bar',
214
+ data: {
215
+ labels: ['Joy', 'Anger', 'Fear', 'Sadness'],
216
+ datasets: [{
217
+ label: 'Emotion Count',
218
+ data: [40, 25, 15, 20], // initial mock data
219
+ backgroundColor: ['#f59e0b', '#ef4444', '#8b5cf6', '#3b82f6'],
220
+ borderRadius: 4
221
+ }]
222
+ },
223
+ options: {
224
+ responsive: true,
225
+ maintainAspectRatio: false,
226
+ plugins: {
227
+ legend: { display: false },
228
+ title: { display: true, text: 'Emotion Frequency', color: textColor }
229
+ },
230
+ scales: {
231
+ y: { beginAtZero: true, grid: { color: 'rgba(156, 163, 175, 0.1)' }, ticks: { color: textColor } },
232
+ x: { grid: { display: false }, ticks: { color: textColor } }
233
+ }
234
+ }
235
+ });
236
+ }
237
+
238
+ function updateChartColors() {
239
+ const textColor = getComputedStyle(document.body).getPropertyValue('--text-primary').trim() || '#111827';
240
+ if (sentimentChart) {
241
+ sentimentChart.options.plugins.legend.labels.color = textColor;
242
+ sentimentChart.options.plugins.title.color = textColor;
243
+ sentimentChart.update();
244
+ }
245
+ if (emotionChart) {
246
+ emotionChart.options.plugins.title.color = textColor;
247
+ emotionChart.options.scales.x.ticks.color = textColor;
248
+ emotionChart.options.scales.y.ticks.color = textColor;
249
+ emotionChart.update();
250
+ }
251
+ }
252
+
253
+ function updateAnalyticsMock() {
254
+ if (sentimentChart && emotionChart) {
255
+ // Slightly jitter data for demo
256
+ const sData = sentimentChart.data.datasets[0].data;
257
+ sData[0] += Math.floor(Math.random() * 3);
258
+ sData[1] += Math.floor(Math.random() * 3);
259
+ sData[2] += Math.floor(Math.random() * 3);
260
+ sentimentChart.update();
261
+
262
+ const eData = emotionChart.data.datasets[0].data;
263
+ eData[Math.floor(Math.random() * 4)] += 1;
264
+ emotionChart.update();
265
+ }
266
+ }
267
+
268
+ // Live Feed Simulation
269
+ const mockTweets = [
270
+ { text: "آج کا دن بہت اچھا گزر رہا ہے! 🌞", user: "@ahmed_pk", pos: true, neu: false, neg: false },
271
+ { text: "traffic ne bohat tang kiya hua hai aaj 😡", user: "@sana_tweets", pos: false, neu: false, neg: true },
272
+ { text: "Weather update: It might rain tomorrow in Lahore.", user: "@news_update", pos: false, neu: true, neg: false },
273
+ { text: "مجھے ڈر ہے کہ کل کا امتحان کیسا ہوگا 😰", user: "@student_life", pos: false, neu: false, neg: true },
274
+ { text: "ye movie bohat zbardast thi, highly recommended! 🎬", user: "@cinephile", pos: true, neu: false, neg: false }
275
+ ];
276
+
277
+ function startLiveFeed() {
278
+ const container = document.getElementById('liveFeed');
279
+
280
+ // Initial tweets
281
+ for (let i = 0; i < 3; i++) {
282
+ addTweetToFeed(container, mockTweets[i]);
283
+ }
284
+
285
+ // Add new tweet periodically
286
+ setInterval(() => {
287
+ const tweet = mockTweets[Math.floor(Math.random() * mockTweets.length)];
288
+ addTweetToFeed(container, tweet);
289
+ }, 8000);
290
+ }
291
+
292
+ function addTweetToFeed(container, data) {
293
+ const el = document.createElement('div');
294
+ el.className = 'tweet';
295
+
296
+ let tagsHtml = '';
297
+ if (data.pos) tagsHtml += '<span class="tag pos">Positive</span>';
298
+ if (data.neg) tagsHtml += '<span class="tag neg">Negative</span>';
299
+ if (data.neu) tagsHtml += '<span class="tag neu">Neutral</span>';
300
+
301
+ el.innerHTML = `
302
+ <div class="tweet-header">
303
+ <span class="tweet-user">${data.user}</span>
304
+ <span class="tweet-time">just now</span>
305
+ </div>
306
+ <div class="tweet-text ${/[\u0600-\u06FF]/.test(data.text) ? 'urdu-text' : ''}">
307
+ ${data.text}
308
+ </div>
309
+ <div class="tweet-tags">
310
+ ${tagsHtml}
311
+ </div>
312
+ `;
313
+
314
+ container.prepend(el);
315
+ if (container.children.length > 10) {
316
+ container.removeChild(container.lastChild);
317
+ }
318
+ }
319
+
320
+ function initKeywords() {
321
+ const keywords = ['اچھا (Good)', 'برا (Bad)', 'خوش (Happy)', 'traffic', 'movie', 'امتحان', 'khushi', 'zbardast', 'غصہ', 'barish'];
322
+ const container = document.getElementById('trendingKeywords');
323
+
324
+ keywords.forEach(kw => {
325
+ const span = document.createElement('span');
326
+ span.className = 'keyword';
327
+ span.textContent = kw;
328
+ span.onclick = () => {
329
+ document.getElementById('textInput').value = kw.split(' ')[0];
330
+ document.getElementById('analyzeBtn').click();
331
+ };
332
+ container.appendChild(span);
333
+ });
334
+ }
static/style.css ADDED
@@ -0,0 +1,532 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ /* Dark Theme (Default) */
3
+ --bg-color: #0b0f19;
4
+ --surface: #111827;
5
+ --surface-hover: #1f2937;
6
+ --border: #374151;
7
+ --text-primary: #f9fafb;
8
+ --text-secondary: #9ca3af;
9
+ --primary: #6366f1;
10
+ --primary-hover: #4f46e5;
11
+
12
+ /* Sentiments */
13
+ --positive: #10b981;
14
+ --negative: #ef4444;
15
+ --neutral: #6b7280;
16
+
17
+ /* Emotions */
18
+ --joy: #f59e0b;
19
+ --anger: #ef4444;
20
+ --fear: #8b5cf6;
21
+ --sadness: #3b82f6;
22
+
23
+ /* Effects */
24
+ --glass-bg: rgba(17, 24, 39, 0.7);
25
+ --glass-border: rgba(255, 255, 255, 0.1);
26
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
27
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
28
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
29
+ }
30
+
31
+ /* Light Theme Variables */
32
+ body.light-theme {
33
+ --bg-color: #f3f4f6;
34
+ --surface: #ffffff;
35
+ --surface-hover: #f9fafb;
36
+ --border: #e5e7eb;
37
+ --text-primary: #111827;
38
+ --text-secondary: #4b5563;
39
+ --glass-bg: rgba(255, 255, 255, 0.7);
40
+ --glass-border: rgba(0, 0, 0, 0.1);
41
+ }
42
+
43
+ * {
44
+ margin: 0;
45
+ padding: 0;
46
+ box-sizing: border-box;
47
+ }
48
+
49
+ body {
50
+ font-family: 'Inter', sans-serif;
51
+ background-color: var(--bg-color);
52
+ color: var(--text-primary);
53
+ min-height: 100vh;
54
+ transition: background-color 0.3s ease, color 0.3s ease;
55
+ }
56
+
57
+ .urdu-text {
58
+ font-family: 'Noto Nastaliq Urdu', serif;
59
+ direction: rtl;
60
+ }
61
+
62
+ .urdu-input {
63
+ font-family: 'Noto Nastaliq Urdu', serif;
64
+ }
65
+
66
+ /* App Container */
67
+ .app-container {
68
+ max-width: 1400px;
69
+ margin: 0 auto;
70
+ padding: 0 1rem;
71
+ }
72
+
73
+ /* Header */
74
+ .glass-header {
75
+ display: flex;
76
+ justify-content: space-between;
77
+ align-items: center;
78
+ padding: 1rem 2rem;
79
+ margin: 1rem 0 2rem 0;
80
+ background: var(--glass-bg);
81
+ backdrop-filter: blur(12px);
82
+ border: 1px solid var(--glass-border);
83
+ border-radius: 1rem;
84
+ position: sticky;
85
+ top: 1rem;
86
+ z-index: 50;
87
+ box-shadow: var(--shadow-md);
88
+ }
89
+
90
+ .logo {
91
+ display: flex;
92
+ align-items: center;
93
+ gap: 1rem;
94
+ }
95
+
96
+ .logo-icon {
97
+ width: 48px;
98
+ height: 48px;
99
+ background: linear-gradient(135deg, var(--primary), var(--fear));
100
+ border-radius: 12px;
101
+ display: flex;
102
+ align-items: center;
103
+ justify-content: center;
104
+ font-size: 1.25rem;
105
+ font-weight: bold;
106
+ color: white;
107
+ font-family: 'Noto Nastaliq Urdu', serif;
108
+ }
109
+
110
+ .logo-text h1 {
111
+ font-size: 1.25rem;
112
+ font-weight: 700;
113
+ margin: 0;
114
+ background: linear-gradient(to right, #6366f1, #8b5cf6);
115
+ -webkit-background-clip: text;
116
+ -webkit-text-fill-color: transparent;
117
+ }
118
+
119
+ .badge {
120
+ font-size: 0.75rem;
121
+ padding: 0.2rem 0.5rem;
122
+ background: rgba(99, 102, 241, 0.2);
123
+ color: #818cf8;
124
+ border-radius: 99px;
125
+ font-weight: 600;
126
+ }
127
+
128
+ .icon-btn {
129
+ background: transparent;
130
+ border: none;
131
+ color: var(--text-primary);
132
+ width: 40px;
133
+ height: 40px;
134
+ border-radius: 50%;
135
+ cursor: pointer;
136
+ display: flex;
137
+ align-items: center;
138
+ justify-content: center;
139
+ transition: background 0.2s;
140
+ }
141
+
142
+ .icon-btn:hover {
143
+ background: var(--surface-hover);
144
+ }
145
+
146
+ .icon-btn svg {
147
+ width: 20px;
148
+ height: 20px;
149
+ }
150
+
151
+ /* Dashboard Layout */
152
+ .dashboard {
153
+ display: grid;
154
+ grid-template-columns: 1fr 350px;
155
+ gap: 2rem;
156
+ padding-bottom: 2rem;
157
+ }
158
+
159
+ @media (max-width: 1024px) {
160
+ .dashboard {
161
+ grid-template-columns: 1fr;
162
+ }
163
+ }
164
+
165
+ /* Cards */
166
+ .card {
167
+ background: var(--surface);
168
+ border: 1px solid var(--border);
169
+ border-radius: 1rem;
170
+ padding: 1.5rem;
171
+ box-shadow: var(--shadow-md);
172
+ transition: transform 0.2s, box-shadow 0.2s;
173
+ }
174
+
175
+ .card:hover {
176
+ box-shadow: var(--shadow-lg);
177
+ }
178
+
179
+ .card-header {
180
+ display: flex;
181
+ justify-content: space-between;
182
+ align-items: center;
183
+ margin-bottom: 1rem;
184
+ }
185
+
186
+ .card-header h2 {
187
+ font-size: 1.125rem;
188
+ font-weight: 600;
189
+ }
190
+
191
+ /* Input Section */
192
+ .input-wrapper {
193
+ position: relative;
194
+ margin-bottom: 1rem;
195
+ }
196
+
197
+ textarea {
198
+ width: 100%;
199
+ min-height: 120px;
200
+ background: var(--bg-color);
201
+ border: 1px solid var(--border);
202
+ border-radius: 0.75rem;
203
+ padding: 1rem;
204
+ color: var(--text-primary);
205
+ font-size: 1rem;
206
+ resize: vertical;
207
+ transition: border-color 0.2s, box-shadow 0.2s;
208
+ }
209
+
210
+ textarea:focus {
211
+ outline: none;
212
+ border-color: var(--primary);
213
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
214
+ }
215
+
216
+ .actions {
217
+ display: flex;
218
+ justify-content: flex-end;
219
+ }
220
+
221
+ .primary-btn {
222
+ background: var(--primary);
223
+ color: white;
224
+ border: none;
225
+ padding: 0.75rem 1.5rem;
226
+ border-radius: 0.75rem;
227
+ font-size: 1rem;
228
+ font-weight: 600;
229
+ cursor: pointer;
230
+ display: flex;
231
+ align-items: center;
232
+ gap: 0.5rem;
233
+ transition: background-color 0.2s, transform 0.1s;
234
+ }
235
+
236
+ .primary-btn:hover {
237
+ background: var(--primary-hover);
238
+ transform: translateY(-1px);
239
+ }
240
+
241
+ .primary-btn:active {
242
+ transform: translateY(0);
243
+ }
244
+
245
+ .primary-btn svg {
246
+ width: 20px;
247
+ height: 20px;
248
+ }
249
+
250
+ .primary-btn:disabled {
251
+ opacity: 0.7;
252
+ cursor: not-allowed;
253
+ transform: none;
254
+ }
255
+
256
+ .spinner {
257
+ animation: spin 1s linear infinite;
258
+ }
259
+
260
+ @keyframes spin {
261
+ from { transform: rotate(0deg); }
262
+ to { transform: rotate(360deg); }
263
+ }
264
+
265
+ /* Language Badge */
266
+ .language-badge {
267
+ background: rgba(99, 102, 241, 0.1);
268
+ color: var(--primary);
269
+ padding: 0.25rem 0.75rem;
270
+ border-radius: 99px;
271
+ font-size: 0.875rem;
272
+ font-weight: 500;
273
+ }
274
+
275
+ /* Results Section */
276
+ .results-card {
277
+ margin-top: 2rem;
278
+ animation: slideUp 0.4s ease-out;
279
+ }
280
+
281
+ @keyframes slideUp {
282
+ from { opacity: 0; transform: translateY(20px); }
283
+ to { opacity: 1; transform: translateY(0); }
284
+ }
285
+
286
+ .results-grid {
287
+ display: grid;
288
+ grid-template-columns: 1fr 1fr;
289
+ gap: 1.5rem;
290
+ }
291
+
292
+ @media (max-width: 640px) {
293
+ .results-grid {
294
+ grid-template-columns: 1fr;
295
+ }
296
+ }
297
+
298
+ .result-box {
299
+ background: var(--bg-color);
300
+ border: 1px solid var(--border);
301
+ border-radius: 0.75rem;
302
+ padding: 1.25rem;
303
+ text-align: center;
304
+ }
305
+
306
+ .result-box h3 {
307
+ font-size: 0.875rem;
308
+ color: var(--text-secondary);
309
+ text-transform: uppercase;
310
+ letter-spacing: 0.05em;
311
+ margin-bottom: 1rem;
312
+ }
313
+
314
+ .primary-result {
315
+ display: flex;
316
+ flex-direction: column;
317
+ align-items: center;
318
+ gap: 0.5rem;
319
+ margin-bottom: 1rem;
320
+ }
321
+
322
+ .primary-result .emoji {
323
+ font-size: 2.5rem;
324
+ line-height: 1;
325
+ }
326
+
327
+ .primary-result .label {
328
+ font-size: 1.25rem;
329
+ font-weight: 600;
330
+ }
331
+
332
+ .confidence-bar {
333
+ height: 6px;
334
+ background: var(--surface);
335
+ border-radius: 99px;
336
+ overflow: hidden;
337
+ margin-bottom: 0.5rem;
338
+ }
339
+
340
+ .confidence-bar .fill {
341
+ height: 100%;
342
+ background: var(--primary);
343
+ border-radius: 99px;
344
+ transition: width 1s cubic-bezier(0.4, 0, 0.2, 1);
345
+ }
346
+
347
+ .conf-text {
348
+ font-size: 0.75rem;
349
+ color: var(--text-secondary);
350
+ }
351
+
352
+ /* Attention Map */
353
+ .attention-box {
354
+ margin-top: 1.5rem;
355
+ background: var(--bg-color);
356
+ border: 1px solid var(--border);
357
+ border-radius: 0.75rem;
358
+ padding: 1.25rem;
359
+ }
360
+
361
+ .attention-box h3 {
362
+ font-size: 0.875rem;
363
+ color: var(--text-secondary);
364
+ margin-bottom: 1rem;
365
+ display: flex;
366
+ align-items: center;
367
+ gap: 0.5rem;
368
+ }
369
+
370
+ .info-icon {
371
+ cursor: help;
372
+ color: var(--primary);
373
+ }
374
+
375
+ .attention-content {
376
+ font-size: 1.5rem;
377
+ line-height: 2;
378
+ padding: 1rem;
379
+ }
380
+
381
+ .attention-word {
382
+ padding: 0.1rem 0.2rem;
383
+ border-radius: 0.25rem;
384
+ transition: background-color 0.3s;
385
+ }
386
+
387
+ /* Analytics */
388
+ .analytics-card {
389
+ margin-top: 2rem;
390
+ }
391
+
392
+ .charts-container {
393
+ display: grid;
394
+ grid-template-columns: 1fr 1fr;
395
+ gap: 1.5rem;
396
+ margin-top: 1.5rem;
397
+ }
398
+
399
+ .chart-wrapper {
400
+ position: relative;
401
+ height: 250px;
402
+ width: 100%;
403
+ }
404
+
405
+ @media (max-width: 768px) {
406
+ .charts-container {
407
+ grid-template-columns: 1fr;
408
+ }
409
+ }
410
+
411
+ /* Sidebar Elements */
412
+ .mt-4 {
413
+ margin-top: 1.5rem;
414
+ }
415
+
416
+ .live-indicator {
417
+ display: flex;
418
+ align-items: center;
419
+ gap: 0.5rem;
420
+ }
421
+
422
+ .dot {
423
+ width: 8px;
424
+ height: 8px;
425
+ background-color: #ef4444;
426
+ border-radius: 50%;
427
+ animation: pulse 2s infinite;
428
+ }
429
+
430
+ @keyframes pulse {
431
+ 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); }
432
+ 70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); }
433
+ 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
434
+ }
435
+
436
+ .feed-container {
437
+ display: flex;
438
+ flex-direction: column;
439
+ gap: 1rem;
440
+ max-height: 400px;
441
+ overflow-y: auto;
442
+ padding-right: 0.5rem;
443
+ }
444
+
445
+ .feed-container::-webkit-scrollbar {
446
+ width: 6px;
447
+ }
448
+
449
+ .feed-container::-webkit-scrollbar-track {
450
+ background: transparent;
451
+ }
452
+
453
+ .feed-container::-webkit-scrollbar-thumb {
454
+ background: var(--border);
455
+ border-radius: 3px;
456
+ }
457
+
458
+ .tweet {
459
+ background: var(--bg-color);
460
+ border: 1px solid var(--border);
461
+ padding: 1rem;
462
+ border-radius: 0.75rem;
463
+ animation: slideInRight 0.3s ease-out;
464
+ }
465
+
466
+ @keyframes slideInRight {
467
+ from { opacity: 0; transform: translateX(20px); }
468
+ to { opacity: 1; transform: translateX(0); }
469
+ }
470
+
471
+ .tweet-header {
472
+ display: flex;
473
+ justify-content: space-between;
474
+ margin-bottom: 0.5rem;
475
+ font-size: 0.875rem;
476
+ }
477
+
478
+ .tweet-user {
479
+ font-weight: 600;
480
+ color: var(--primary);
481
+ }
482
+
483
+ .tweet-time {
484
+ color: var(--text-secondary);
485
+ }
486
+
487
+ .tweet-text {
488
+ font-size: 1rem;
489
+ line-height: 1.5;
490
+ direction: auto;
491
+ }
492
+
493
+ .tweet-tags {
494
+ display: flex;
495
+ gap: 0.5rem;
496
+ margin-top: 0.75rem;
497
+ }
498
+
499
+ .tag {
500
+ font-size: 0.75rem;
501
+ padding: 0.1rem 0.5rem;
502
+ border-radius: 99px;
503
+ background: var(--surface);
504
+ border: 1px solid var(--border);
505
+ }
506
+
507
+ .tag.pos { color: var(--positive); border-color: var(--positive); }
508
+ .tag.neg { color: var(--negative); border-color: var(--negative); }
509
+ .tag.neu { color: var(--neutral); border-color: var(--neutral); }
510
+
511
+ /* Keywords */
512
+ .tags-cloud {
513
+ display: flex;
514
+ flex-wrap: wrap;
515
+ gap: 0.5rem;
516
+ }
517
+
518
+ .keyword {
519
+ background: rgba(99, 102, 241, 0.1);
520
+ color: var(--text-primary);
521
+ padding: 0.4rem 0.8rem;
522
+ border-radius: 0.5rem;
523
+ font-size: 0.875rem;
524
+ cursor: pointer;
525
+ transition: background 0.2s, transform 0.2s;
526
+ border: 1px solid rgba(99, 102, 241, 0.2);
527
+ }
528
+
529
+ .keyword:hover {
530
+ background: rgba(99, 102, 241, 0.2);
531
+ transform: scale(1.05);
532
+ }
test_models.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ import numpy as np
4
+ from transformers import AutoTokenizer ,AutoModelForSequenceClassification
5
+
6
+ def main ():
7
+ print ("="*60 )
8
+ print (" Loading Urdu Sentiment & Emotion Models...")
9
+ print ("="*60 )
10
+
11
+ base_dir =os .path .dirname (os .path .abspath (__file__ ))
12
+ sentiment_dir =os .path .join (base_dir ,"models","sentiment_model")
13
+ emotion_dir =os .path .join (base_dir ,"models","emotion_model")
14
+
15
+
16
+
17
+ sentiment_map ={0 :"Positive 😊",1 :"Negative 😠",2 :"Neutral 😐"}
18
+ emotion_map ={0 :"Joy 😄",1 :"Anger 😡",2 :"Fear 😨",3 :"Sadness 😢"}
19
+
20
+
21
+ try :
22
+ print ("Loading Tokenizer...")
23
+ tokenizer =AutoTokenizer .from_pretrained (sentiment_dir )
24
+
25
+ print ("Loading Sentiment Model...")
26
+ sentiment_model =AutoModelForSequenceClassification .from_pretrained (sentiment_dir )
27
+
28
+ print ("Loading Emotion Model...")
29
+ emotion_model =AutoModelForSequenceClassification .from_pretrained (emotion_dir )
30
+ except Exception as e :
31
+ print (f"Error loading models. Are you sure they finished training? ({e })")
32
+ return
33
+
34
+ print ("\n✅ Models loaded successfully!")
35
+ print ("Type an Urdu sentence (Roman or Script) to test them. Type 'exit' to quit.\n")
36
+
37
+
38
+ while True :
39
+ text =input ("Enter Urdu text: ")
40
+ if text .strip ().lower ()in ['exit','quit','q']:
41
+ break
42
+ if not text .strip ():
43
+ continue
44
+
45
+
46
+ inputs =tokenizer (text ,return_tensors ="pt",truncation =True ,max_length =128 )
47
+
48
+
49
+ with torch .no_grad ():
50
+ sentiment_out =sentiment_model (**inputs ).logits
51
+ emotion_out =emotion_model (**inputs ).logits
52
+
53
+
54
+ sentiment_idx =np .argmax (sentiment_out .numpy (),axis =-1 )[0 ]
55
+ emotion_idx =np .argmax (emotion_out .numpy (),axis =-1 )[0 ]
56
+
57
+ print ("-"*40 )
58
+ print (f"Sentiment : {sentiment_map [sentiment_idx ]}")
59
+ print (f"Emotion : {emotion_map [emotion_idx ]}")
60
+ print ("-"*40 +"\n")
61
+
62
+ if __name__ =="__main__":
63
+ main ()
training/dataset.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from torch .utils .data import Dataset
3
+ import pandas as pd
4
+
5
+
6
+ class UrduTextDataset (Dataset ):
7
+ def __init__ (self ,csv_paths ,tokenizer ,max_length =128 ,task ="sentiment"):
8
+ """
9
+ Custom PyTorch Dataset for Urdu Sentiment and Emotion Analysis.
10
+
11
+ NOTE: Tokenization here does NOT pad. Padding is applied per-batch by a
12
+ DataCollatorWithPadding in the training script (dynamic padding), which
13
+ is much faster than padding every sample to max_length -- especially on
14
+ CPU, where wasted FLOPs on padding tokens dominate runtime.
15
+
16
+ Args:
17
+ csv_paths (list or str): Path(s) to the cleaned CSV files.
18
+ tokenizer: HuggingFace tokenizer (XLM-RoBERTa).
19
+ max_length (int): Max token length for truncation.
20
+ task (str): 'sentiment' or 'emotion'.
21
+ """
22
+ self .tokenizer =tokenizer
23
+ self .max_length =max_length
24
+ self .task =task
25
+
26
+ if isinstance (csv_paths ,str ):
27
+ csv_paths =[csv_paths ]
28
+
29
+
30
+ dfs =[pd .read_csv (path )for path in csv_paths ]
31
+ self .data =pd .concat (dfs ,ignore_index =True )
32
+
33
+ before =len (self .data )
34
+
35
+ self .data =self .data .dropna (subset =['text','label'])
36
+
37
+
38
+ if task =="sentiment":
39
+
40
+ label_map ={
41
+ 'P':2 ,'O':1 ,'N':0 ,
42
+ 2 :2 ,1 :1 ,0 :0 ,
43
+ '2':2 ,'1':1 ,'0':0 ,
44
+ }
45
+ elif task =="emotion":
46
+
47
+ label_map ={'joy':0 ,'anger':1 ,'fear':2 ,'sadness':3 }
48
+ else :
49
+ raise ValueError ("Task must be either 'sentiment' or 'emotion'")
50
+
51
+ self .data ['label']=self .data ['label'].map (label_map )
52
+
53
+
54
+ unmapped =int (self .data ['label'].isna ().sum ())
55
+ self .data =self .data .dropna (subset =['label'])
56
+ self .data ['label']=self .data ['label'].astype (int )
57
+ after =len (self .data )
58
+ print (f" [{task }] kept {after }/{before } rows "
59
+ f"({before -after } dropped, {unmapped } had invalid labels)")
60
+ print (f" [{task }] class counts: "
61
+ f"{self .data ['label'].value_counts ().sort_index ().to_dict ()}")
62
+
63
+ self .texts =self .data ['text'].astype (str ).tolist ()
64
+ self .labels =self .data ['label'].tolist ()
65
+
66
+ def __len__ (self ):
67
+ return len (self .texts )
68
+
69
+ def __getitem__ (self ,idx ):
70
+
71
+
72
+ encoding =self .tokenizer (
73
+ self .texts [idx ],
74
+ add_special_tokens =True ,
75
+ max_length =self .max_length ,
76
+ truncation =True ,
77
+ )
78
+ return {
79
+ 'input_ids':encoding ['input_ids'],
80
+ 'attention_mask':encoding ['attention_mask'],
81
+ 'labels':torch .tensor (self .labels [idx ],dtype =torch .long ),
82
+ }
83
+
84
+ def get_class_weights (self ,num_labels ):
85
+ """Inverse-frequency class weights for a weighted loss (handles imbalance)."""
86
+ counts =self .data ['label'].value_counts ().sort_index ()
87
+ counts =counts .reindex (range (num_labels ),fill_value =0 )
88
+ total =counts .sum ()
89
+
90
+ weights =total /(num_labels *counts .replace (0 ,1 ))
91
+ return torch .tensor (weights .values ,dtype =torch .float )
training/phase3_verify.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+
5
+ sys .path .append (os .path .dirname (os .path .abspath (__file__ )))
6
+
7
+ from transformers import AutoTokenizer
8
+ from dataset import UrduTextDataset
9
+ from torch .utils .data import DataLoader
10
+
11
+ def main ():
12
+ print ("Loading XLM-RoBERTa Tokenizer...")
13
+ tokenizer =AutoTokenizer .from_pretrained ("xlm-roberta-base")
14
+
15
+ data_dir =os .path .join (os .path .dirname (os .path .dirname (os .path .abspath (__file__ ))),'data')
16
+
17
+ print ("\n--- Testing Sentiment Dataset ---")
18
+ sentiment_train_files =[
19
+ os .path .join (data_dir ,'roman_urdu_sentiment_train.csv'),
20
+ os .path .join (data_dir ,'urdu_sentiment_corpus_train.csv')
21
+ ]
22
+
23
+ sentiment_dataset =UrduTextDataset (
24
+ csv_paths =sentiment_train_files ,
25
+ tokenizer =tokenizer ,
26
+ max_length =128 ,
27
+ task ="sentiment"
28
+ )
29
+
30
+ print (f"Total Sentiment Training Samples: {len (sentiment_dataset )}")
31
+
32
+
33
+ sample =sentiment_dataset [0 ]
34
+ print (f"\nSample Text: {sample ['text']}")
35
+ print (f"Sample Label: {sample ['label']} (0=Neg, 1=Neu, 2=Pos)")
36
+ print (f"Input IDs shape: {sample ['input_ids'].shape }")
37
+ print (f"Attention Mask shape: {sample ['attention_mask'].shape }")
38
+
39
+
40
+ loader =DataLoader (sentiment_dataset ,batch_size =4 ,shuffle =True )
41
+ batch =next (iter (loader ))
42
+ print (f"\nBatch Input IDs shape: {batch ['input_ids'].shape }")
43
+ print (f"Batch Labels: {batch ['label']}")
44
+
45
+ print ("\n--- Testing Emotion Dataset ---")
46
+ emotion_train_files =[
47
+ os .path .join (data_dir ,'semeval_emotion_train.csv')
48
+ ]
49
+
50
+ emotion_dataset =UrduTextDataset (
51
+ csv_paths =emotion_train_files ,
52
+ tokenizer =tokenizer ,
53
+ max_length =128 ,
54
+ task ="emotion"
55
+ )
56
+
57
+ print (f"Total Emotion Training Samples: {len (emotion_dataset )}")
58
+ emotion_sample =emotion_dataset [0 ]
59
+ print (f"Sample Label: {emotion_sample ['label']} (0=Joy, 1=Anger, 2=Fear, 3=Sadness)")
60
+
61
+ print ("\nPhase 3 Verification Successful! Dataset class and tokenization work perfectly.")
62
+
63
+ if __name__ =="__main__":
64
+ main ()
training/train_emotion.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import numpy as np
4
+ import torch
5
+ import torch .nn as nn
6
+ from transformers import (
7
+ AutoTokenizer ,AutoModelForSequenceClassification ,
8
+ Trainer ,TrainingArguments ,DataCollatorWithPadding ,
9
+ EarlyStoppingCallback ,
10
+ )
11
+ from sklearn .metrics import accuracy_score ,f1_score ,precision_recall_fscore_support
12
+
13
+
14
+ sys .path .append (os .path .dirname (os .path .abspath (__file__ )))
15
+ from dataset import UrduTextDataset
16
+
17
+ NUM_LABELS =4
18
+ MAX_LENGTH =128
19
+
20
+
21
+ def compute_metrics (eval_pred ):
22
+ logits ,labels =eval_pred
23
+ preds =np .argmax (logits ,axis =-1 )
24
+ precision ,recall ,f1 ,_ =precision_recall_fscore_support (
25
+ labels ,preds ,average ='weighted',zero_division =0 )
26
+ macro_f1 =f1_score (labels ,preds ,average ='macro',zero_division =0 )
27
+ return {
28
+ 'accuracy':accuracy_score (labels ,preds ),
29
+ 'f1':f1 ,
30
+ 'macro_f1':macro_f1 ,
31
+ 'precision':precision ,
32
+ 'recall':recall ,
33
+ }
34
+
35
+
36
+ class WeightedTrainer (Trainer ):
37
+ """Trainer that applies class weights (+ optional label smoothing) in the loss."""
38
+ def __init__ (self ,*args ,class_weights =None ,label_smoothing =0.0 ,**kwargs ):
39
+ super ().__init__ (*args ,**kwargs )
40
+ self .class_weights =class_weights
41
+ self .label_smoothing =label_smoothing
42
+
43
+ def compute_loss (self ,model ,inputs ,return_outputs =False ,**kwargs ):
44
+ labels =inputs .pop ("labels")
45
+ outputs =model (**inputs )
46
+ weight =(self .class_weights .to (model .device )
47
+ if self .class_weights is not None else None )
48
+ loss_fn =nn .CrossEntropyLoss (
49
+ weight =weight ,label_smoothing =self .label_smoothing )
50
+ loss =loss_fn (outputs .logits .view (-1 ,NUM_LABELS ),labels .view (-1 ))
51
+ return (loss ,outputs )if return_outputs else loss
52
+
53
+
54
+ def main ():
55
+ print ("="*60 )
56
+ print (" Phase 4: Training Emotion Model")
57
+ print ("="*60 )
58
+
59
+
60
+ torch .set_num_threads (os .cpu_count ()or 1 )
61
+
62
+ model_name ="xlm-roberta-base"
63
+
64
+
65
+ print ("\n[1/5] Loading tokenizer and XLM-RoBERTa model...")
66
+ tokenizer =AutoTokenizer .from_pretrained (model_name )
67
+ model =AutoModelForSequenceClassification .from_pretrained (
68
+ model_name ,num_labels =NUM_LABELS )
69
+
70
+
71
+ print ("\n[2/5] Loading datasets...")
72
+ base =os .path .dirname (os .path .dirname (os .path .abspath (__file__ )))
73
+ data_dir =os .path .join (base ,'data')
74
+ train_files =[os .path .join (data_dir ,'semeval_emotion_train.csv')]
75
+ val_files =[os .path .join (data_dir ,'semeval_emotion_val.csv')]
76
+
77
+ train_dataset =UrduTextDataset (train_files ,tokenizer ,max_length =MAX_LENGTH ,task ="emotion")
78
+ val_dataset =UrduTextDataset (val_files ,tokenizer ,max_length =MAX_LENGTH ,task ="emotion")
79
+ print (f"Train samples: {len (train_dataset )} | Val samples: {len (val_dataset )}")
80
+
81
+
82
+ class_weights =train_dataset .get_class_weights (NUM_LABELS )
83
+ print (f"Class weights: {class_weights .tolist ()}")
84
+ data_collator =DataCollatorWithPadding (tokenizer =tokenizer )
85
+
86
+
87
+ print ("\n[3/5] Setting up Training Arguments...")
88
+ output_dir =os .path .join (base ,'models','emotion_model')
89
+
90
+ training_args =TrainingArguments (
91
+ output_dir =output_dir ,
92
+ num_train_epochs =6 ,
93
+ per_device_train_batch_size =8 ,
94
+ per_device_eval_batch_size =16 ,
95
+ gradient_accumulation_steps =4 ,
96
+ learning_rate =2e-5 ,
97
+ warmup_ratio =0.1 ,
98
+ weight_decay =0.01 ,
99
+ lr_scheduler_type ="cosine",
100
+ max_grad_norm =1.0 ,
101
+
102
+ dataloader_pin_memory =False ,
103
+ dataloader_num_workers =0 ,
104
+ logging_dir ='./logs/emotion',
105
+ logging_steps =50 ,
106
+ eval_strategy ="epoch",
107
+ save_strategy ="epoch",
108
+ save_total_limit =2 ,
109
+ load_best_model_at_end =True ,
110
+ metric_for_best_model ="macro_f1",
111
+ greater_is_better =True ,
112
+ seed =42 ,
113
+ report_to ="none",
114
+ )
115
+
116
+
117
+ print ("\n[4/5] Initializing Trainer...")
118
+ trainer =WeightedTrainer (
119
+ model =model ,
120
+ args =training_args ,
121
+ train_dataset =train_dataset ,
122
+ eval_dataset =val_dataset ,
123
+ processing_class =tokenizer ,
124
+ data_collator =data_collator ,
125
+ compute_metrics =compute_metrics ,
126
+ class_weights =class_weights ,
127
+ label_smoothing =0.1 ,
128
+ callbacks =[EarlyStoppingCallback (early_stopping_patience =2 )],
129
+ )
130
+
131
+
132
+ print ("\n[5/5] Starting training loop...")
133
+ trainer .train ()
134
+
135
+ print (f"\nTraining complete! Saving best model to {output_dir }")
136
+ trainer .save_model (output_dir )
137
+ tokenizer .save_pretrained (output_dir )
138
+ print ("Emotion model saved successfully!")
139
+
140
+
141
+ if __name__ =="__main__":
142
+ main ()
training/train_sentiment.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import numpy as np
4
+ import torch
5
+ import torch .nn as nn
6
+ from transformers import (
7
+ AutoTokenizer ,AutoModelForSequenceClassification ,
8
+ Trainer ,TrainingArguments ,DataCollatorWithPadding ,
9
+ EarlyStoppingCallback ,
10
+ )
11
+ from sklearn .metrics import accuracy_score ,f1_score ,precision_recall_fscore_support
12
+
13
+
14
+ sys .path .append (os .path .dirname (os .path .abspath (__file__ )))
15
+ from dataset import UrduTextDataset
16
+
17
+ NUM_LABELS =3
18
+ MAX_LENGTH =128
19
+
20
+
21
+ def compute_metrics (eval_pred ):
22
+ logits ,labels =eval_pred
23
+ preds =np .argmax (logits ,axis =-1 )
24
+ precision ,recall ,f1 ,_ =precision_recall_fscore_support (
25
+ labels ,preds ,average ='weighted',zero_division =0 )
26
+ macro_f1 =f1_score (labels ,preds ,average ='macro',zero_division =0 )
27
+ return {
28
+ 'accuracy':accuracy_score (labels ,preds ),
29
+ 'f1':f1 ,
30
+ 'macro_f1':macro_f1 ,
31
+ 'precision':precision ,
32
+ 'recall':recall ,
33
+ }
34
+
35
+
36
+ class WeightedTrainer (Trainer ):
37
+ """Trainer that applies class weights (+ optional label smoothing) in the loss."""
38
+ def __init__ (self ,*args ,class_weights =None ,label_smoothing =0.0 ,**kwargs ):
39
+ super ().__init__ (*args ,**kwargs )
40
+ self .class_weights =class_weights
41
+ self .label_smoothing =label_smoothing
42
+
43
+ def compute_loss (self ,model ,inputs ,return_outputs =False ,**kwargs ):
44
+ labels =inputs .pop ("labels")
45
+ outputs =model (**inputs )
46
+ weight =(self .class_weights .to (model .device )
47
+ if self .class_weights is not None else None )
48
+ loss_fn =nn .CrossEntropyLoss (
49
+ weight =weight ,label_smoothing =self .label_smoothing )
50
+ loss =loss_fn (outputs .logits .view (-1 ,NUM_LABELS ),labels .view (-1 ))
51
+ return (loss ,outputs )if return_outputs else loss
52
+
53
+
54
+ def main ():
55
+ print ("="*60 )
56
+ print (" Phase 4: Training Sentiment Model")
57
+ print ("="*60 )
58
+
59
+
60
+ torch .set_num_threads (os .cpu_count ()or 1 )
61
+
62
+ model_name ="xlm-roberta-base"
63
+
64
+
65
+ print ("\n[1/5] Loading tokenizer and XLM-RoBERTa model...")
66
+ tokenizer =AutoTokenizer .from_pretrained (model_name )
67
+ model =AutoModelForSequenceClassification .from_pretrained (
68
+ model_name ,num_labels =NUM_LABELS )
69
+
70
+
71
+ print ("\n[2/5] Loading datasets...")
72
+ base =os .path .dirname (os .path .dirname (os .path .abspath (__file__ )))
73
+ data_dir =os .path .join (base ,'data')
74
+ train_files =[
75
+ os .path .join (data_dir ,'roman_urdu_sentiment_train.csv'),
76
+ os .path .join (data_dir ,'urdu_sentiment_corpus_train.csv'),
77
+ ]
78
+ val_files =[
79
+ os .path .join (data_dir ,'roman_urdu_sentiment_val.csv'),
80
+ os .path .join (data_dir ,'urdu_sentiment_corpus_val.csv'),
81
+ ]
82
+
83
+ train_dataset =UrduTextDataset (train_files ,tokenizer ,max_length =MAX_LENGTH ,task ="sentiment")
84
+ val_dataset =UrduTextDataset (val_files ,tokenizer ,max_length =MAX_LENGTH ,task ="sentiment")
85
+ print (f"Train samples: {len (train_dataset )} | Val samples: {len (val_dataset )}")
86
+
87
+
88
+ class_weights =train_dataset .get_class_weights (NUM_LABELS )
89
+ print (f"Class weights: {class_weights .tolist ()}")
90
+ data_collator =DataCollatorWithPadding (tokenizer =tokenizer )
91
+
92
+
93
+ print ("\n[3/5] Setting up Training Arguments...")
94
+ output_dir =os .path .join (base ,'models','sentiment_model')
95
+
96
+ training_args =TrainingArguments (
97
+ output_dir =output_dir ,
98
+ num_train_epochs =6 ,
99
+ per_device_train_batch_size =8 ,
100
+ per_device_eval_batch_size =16 ,
101
+ gradient_accumulation_steps =4 ,
102
+ learning_rate =2e-5 ,
103
+ warmup_ratio =0.1 ,
104
+ weight_decay =0.01 ,
105
+ lr_scheduler_type ="cosine",
106
+ max_grad_norm =1.0 ,
107
+
108
+ dataloader_pin_memory =False ,
109
+ dataloader_num_workers =0 ,
110
+ logging_dir ='./logs/sentiment',
111
+ logging_steps =50 ,
112
+ eval_strategy ="epoch",
113
+ save_strategy ="epoch",
114
+ save_total_limit =2 ,
115
+ load_best_model_at_end =True ,
116
+ metric_for_best_model ="macro_f1",
117
+ greater_is_better =True ,
118
+ seed =42 ,
119
+ report_to ="none",
120
+ )
121
+
122
+
123
+ print ("\n[4/5] Initializing Trainer...")
124
+ trainer =WeightedTrainer (
125
+ model =model ,
126
+ args =training_args ,
127
+ train_dataset =train_dataset ,
128
+ eval_dataset =val_dataset ,
129
+ processing_class =tokenizer ,
130
+ data_collator =data_collator ,
131
+ compute_metrics =compute_metrics ,
132
+ class_weights =class_weights ,
133
+ label_smoothing =0.1 ,
134
+ callbacks =[EarlyStoppingCallback (early_stopping_patience =2 )],
135
+ )
136
+
137
+
138
+ print ("\n[5/5] Starting training loop...")
139
+ trainer .train ()
140
+
141
+ print (f"\nTraining complete! Saving best model to {output_dir }")
142
+ trainer .save_model (output_dir )
143
+ tokenizer .save_pretrained (output_dir )
144
+ print ("Sentiment model saved successfully!")
145
+
146
+
147
+ if __name__ =="__main__":
148
+ main ()