hmusman2804045-max commited on
Commit ·
b31728c
0
Parent(s):
Phase 1: Environment setup and training pipeline
Browse files- .gitignore +12 -0
- Dockerfile +0 -0
- README.md +36 -0
- app.py +0 -0
- lang_detector.py +0 -0
- predictor.py +0 -0
- requirements.txt +0 -0
- static/script.js +334 -0
- static/style.css +532 -0
- test_models.py +63 -0
- training/dataset.py +91 -0
- training/phase3_verify.py +64 -0
- training/train_emotion.py +142 -0
- training/train_sentiment.py +148 -0
.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 ()
|