adeyemi001 commited on
Commit
85bcf72
Β·
verified Β·
1 Parent(s): d2bc3b6

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +520 -0
app.py ADDED
@@ -0,0 +1,520 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import joblib
4
+ import torch
5
+ import numpy as np
6
+ from flask import Flask, request, render_template_string, jsonify
7
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
8
+
9
+ app = Flask(__name__)
10
+
11
+ # -----------------------
12
+ # Load artifacts
13
+ # -----------------------
14
+ SAVE_DIR = "./model"
15
+
16
+ try:
17
+ # Load model & tokenizer
18
+ tokenizer = AutoTokenizer.from_pretrained(SAVE_DIR)
19
+ model = AutoModelForSequenceClassification.from_pretrained(SAVE_DIR)
20
+ model.eval()
21
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
22
+ model.to(DEVICE)
23
+
24
+ # Load MultiLabelBinarizer and labels
25
+ mlb = joblib.load(os.path.join(SAVE_DIR, "mlb.joblib"))
26
+ with open(os.path.join(SAVE_DIR, "labels.json"), "r", encoding="utf-8") as f:
27
+ labels = json.load(f)
28
+
29
+ MODEL_LOADED = True
30
+ print(f"Model loaded successfully on device: {DEVICE}")
31
+ print(f"Available labels: {labels}")
32
+
33
+ except Exception as e:
34
+ MODEL_LOADED = False
35
+ print(f"Error loading model: {e}")
36
+ tokenizer = None
37
+ model = None
38
+ mlb = None
39
+ labels = []
40
+
41
+ # Sigmoid for probabilities
42
+ def sigmoid(x):
43
+ return 1 / (1 + np.exp(-x))
44
+
45
+ # -----------------------
46
+ # Prediction function (single text only)
47
+ # -----------------------
48
+ def predict_single(text, threshold=0.5):
49
+ """Predict categories for a single text."""
50
+ if not MODEL_LOADED:
51
+ return [], []
52
+
53
+ # Tokenize
54
+ encodings = tokenizer(
55
+ [text], # Wrap in list since model expects batch
56
+ truncation=True,
57
+ padding=True,
58
+ max_length=256,
59
+ return_tensors="pt"
60
+ ).to(DEVICE)
61
+
62
+ # Forward pass
63
+ with torch.no_grad():
64
+ outputs = model(**encodings)
65
+ logits = outputs.logits.cpu().numpy()
66
+
67
+ # Convert to probabilities
68
+ probs = sigmoid(logits)
69
+
70
+ # Apply fixed threshold (0.5)
71
+ pred_bin = (probs >= threshold).astype(int)
72
+
73
+ # Decode to label names
74
+ row_2d = np.array([pred_bin[0]])
75
+ categories = mlb.inverse_transform(row_2d)[0]
76
+
77
+ return list(categories), probs[0]
78
+
79
+ # HTML Template with embedded CSS + LinkedIn Footer
80
+ HTML_TEMPLATE = """
81
+ <!DOCTYPE html>
82
+ <html lang="en">
83
+ <head>
84
+ <meta charset="UTF-8">
85
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
86
+ <title>Fintech Review Category Classifier</title>
87
+ <style>
88
+ * {
89
+ margin: 0;
90
+ padding: 0;
91
+ box-sizing: border-box;
92
+ }
93
+
94
+ body {
95
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
96
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
97
+ min-height: 100vh;
98
+ padding: 20px;
99
+ display: flex;
100
+ flex-direction: column;
101
+ }
102
+
103
+ .container {
104
+ max-width: 1000px;
105
+ margin: 0 auto;
106
+ background: rgba(255, 255, 255, 0.95);
107
+ border-radius: 20px;
108
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
109
+ backdrop-filter: blur(10px);
110
+ overflow: hidden;
111
+ flex: 1;
112
+ }
113
+
114
+ .header {
115
+ background: linear-gradient(45deg, #2c3e50, #4a6741);
116
+ color: white;
117
+ padding: 30px;
118
+ text-align: center;
119
+ }
120
+
121
+ .header h1 {
122
+ font-size: 2.5em;
123
+ margin-bottom: 10px;
124
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
125
+ }
126
+
127
+ .header p {
128
+ font-size: 1.2em;
129
+ opacity: 0.9;
130
+ }
131
+
132
+ .main-content {
133
+ padding: 40px;
134
+ }
135
+
136
+ .input-section {
137
+ margin-bottom: 30px;
138
+ }
139
+
140
+ .form-group {
141
+ margin-bottom: 20px;
142
+ }
143
+
144
+ label {
145
+ display: block;
146
+ margin-bottom: 10px;
147
+ font-weight: 600;
148
+ color: #333;
149
+ font-size: 1.1em;
150
+ }
151
+
152
+ textarea {
153
+ width: 100%;
154
+ min-height: 120px;
155
+ padding: 15px;
156
+ border: 2px solid #e0e0e0;
157
+ border-radius: 10px;
158
+ font-size: 16px;
159
+ font-family: inherit;
160
+ resize: vertical;
161
+ transition: all 0.3s ease;
162
+ }
163
+
164
+ textarea:focus {
165
+ border-color: #667eea;
166
+ outline: none;
167
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
168
+ }
169
+
170
+ .controls {
171
+ display: flex;
172
+ gap: 20px;
173
+ align-items: center;
174
+ flex-wrap: wrap;
175
+ margin-bottom: 20px;
176
+ }
177
+
178
+ .btn {
179
+ background: linear-gradient(45deg, #667eea, #764ba2);
180
+ color: white;
181
+ border: none;
182
+ padding: 15px 30px;
183
+ font-size: 16px;
184
+ font-weight: 600;
185
+ border-radius: 25px;
186
+ cursor: pointer;
187
+ transition: all 0.3s ease;
188
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
189
+ }
190
+
191
+ .btn:hover {
192
+ transform: translateY(-2px);
193
+ box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
194
+ }
195
+
196
+ .btn:active {
197
+ transform: translateY(0);
198
+ }
199
+
200
+ .btn:disabled {
201
+ opacity: 0.6;
202
+ cursor: not-allowed;
203
+ transform: none;
204
+ }
205
+
206
+ .results-section {
207
+ margin-top: 30px;
208
+ }
209
+
210
+ .result-card {
211
+ background: #f8f9ff;
212
+ border: 1px solid #e0e8ff;
213
+ border-radius: 15px;
214
+ padding: 25px;
215
+ margin-bottom: 20px;
216
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
217
+ }
218
+
219
+ .original-text {
220
+ background: #fff;
221
+ padding: 15px;
222
+ border-radius: 8px;
223
+ border-left: 4px solid #667eea;
224
+ margin-bottom: 20px;
225
+ font-style: italic;
226
+ color: #555;
227
+ }
228
+
229
+ .categories {
230
+ display: flex;
231
+ flex-wrap: wrap;
232
+ gap: 10px;
233
+ margin-bottom: 15px;
234
+ }
235
+
236
+ .category-tag {
237
+ background: linear-gradient(45deg, #48bb78, #38a169);
238
+ color: white;
239
+ padding: 8px 15px;
240
+ border-radius: 20px;
241
+ font-size: 14px;
242
+ font-weight: 500;
243
+ box-shadow: 0 2px 5px rgba(72, 187, 120, 0.3);
244
+ }
245
+
246
+ .no-categories {
247
+ color: #666;
248
+ font-style: italic;
249
+ padding: 10px;
250
+ background: #f0f0f0;
251
+ border-radius: 8px;
252
+ }
253
+
254
+ .loading {
255
+ display: none;
256
+ text-align: center;
257
+ padding: 20px;
258
+ color: #667eea;
259
+ font-size: 18px;
260
+ }
261
+
262
+ .loading.show {
263
+ display: block;
264
+ }
265
+
266
+ .error {
267
+ background: #fed7d7;
268
+ color: #c53030;
269
+ padding: 15px;
270
+ border-radius: 8px;
271
+ margin: 20px 0;
272
+ border-left: 4px solid #c53030;
273
+ }
274
+
275
+ .model-status {
276
+ padding: 15px;
277
+ border-radius: 8px;
278
+ margin-bottom: 20px;
279
+ font-weight: 500;
280
+ }
281
+
282
+ .model-status.loaded {
283
+ background: #c6f6d5;
284
+ color: #22543d;
285
+ border-left: 4px solid #38a169;
286
+ }
287
+
288
+ .model-status.error {
289
+ background: #fed7d7;
290
+ color: #c53030;
291
+ border-left: 4px solid #c53030;
292
+ }
293
+
294
+ footer {
295
+ text-align: center;
296
+ padding: 20px;
297
+ background: #2c3e50;
298
+ color: #ecf0f1;
299
+ font-size: 14px;
300
+ margin-top: auto;
301
+ }
302
+
303
+ footer a {
304
+ color: #667eea;
305
+ text-decoration: none;
306
+ font-weight: 600;
307
+ }
308
+
309
+ footer a:hover {
310
+ text-decoration: underline;
311
+ }
312
+
313
+ @media (max-width: 768px) {
314
+ .header h1 {
315
+ font-size: 2em;
316
+ }
317
+
318
+ .main-content {
319
+ padding: 20px;
320
+ }
321
+
322
+ .controls {
323
+ flex-direction: column;
324
+ align-items: stretch;
325
+ }
326
+ }
327
+ </style>
328
+ </head>
329
+ <body>
330
+ <div class="container">
331
+ <div class="header">
332
+ <h1>🏦 Fintech Review Classifier</h1>
333
+ <p>Classify your customer review into relevant categories</p>
334
+ </div>
335
+
336
+ <div class="main-content">
337
+ {% if model_loaded %}
338
+ <div class="model-status loaded">
339
+ βœ… Model loaded successfully! Available categories: {{ labels|length }}
340
+ </div>
341
+ {% else %}
342
+ <div class="model-status error">
343
+ ❌ Model could not be loaded. Please check if the model files exist in './model' directory.
344
+ </div>
345
+ {% endif %}
346
+
347
+ <form id="classifyForm" {% if not model_loaded %}style="opacity: 0.5; pointer-events: none;"{% endif %}>
348
+ <div class="input-section">
349
+ <div class="form-group">
350
+ <label for="review_text">Enter Customer Review:</label>
351
+ <textarea id="review_text" name="review_text" placeholder="Type a single customer review here..." required>{{ sample_text if sample_text else 'The app crashes every time I try to open it.' }}</textarea>
352
+ </div>
353
+
354
+ <div class="controls">
355
+ <button type="submit" class="btn" {% if not model_loaded %}disabled{% endif %}>
356
+ πŸ” Classify Review
357
+ </button>
358
+ </div>
359
+ </div>
360
+ </form>
361
+
362
+ <div class="loading" id="loading">
363
+ <div>πŸ€– Analyzing review...</div>
364
+ </div>
365
+
366
+ <div class="results-section" id="results" style="display: none;">
367
+ <!-- Results will be inserted here -->
368
+ </div>
369
+ </div>
370
+ </div>
371
+
372
+ <footer>
373
+ Made with ❀️ by Adediran Adeyemi β€” <a href="https://www.linkedin.com/in/adediran-adeyemi-17103b114/" target="_blank">Connect with me on LinkedIn</a>
374
+ </footer>
375
+
376
+ <script>
377
+ // Handle form submission
378
+ document.getElementById('classifyForm').addEventListener('submit', async function(e) {
379
+ e.preventDefault();
380
+
381
+ const formData = new FormData(this);
382
+ const loading = document.getElementById('loading');
383
+ const results = document.getElementById('results');
384
+
385
+ // Show loading, hide results
386
+ loading.classList.add('show');
387
+ results.style.display = 'none';
388
+
389
+ try {
390
+ const response = await fetch('/predict', {
391
+ method: 'POST',
392
+ body: formData
393
+ });
394
+
395
+ const data = await response.json();
396
+
397
+ if (data.error) {
398
+ throw new Error(data.error);
399
+ }
400
+
401
+ displayResults(data);
402
+
403
+ } catch (error) {
404
+ results.innerHTML = '<div class="error">❌ Error: ' + error.message + '</div>';
405
+ results.style.display = 'block';
406
+ } finally {
407
+ loading.classList.remove('show');
408
+ }
409
+ });
410
+
411
+ function displayResults(data) {
412
+ const results = document.getElementById('results');
413
+
414
+ // Clear any existing content completely
415
+ results.innerHTML = '';
416
+
417
+ // Create results header
418
+ const header = document.createElement('h2');
419
+ header.textContent = '🎯 Classification Result';
420
+ results.appendChild(header);
421
+
422
+ // Only one result expected
423
+ const result = data.results[0];
424
+ const card = document.createElement('div');
425
+ card.className = 'result-card';
426
+
427
+ // Original text section
428
+ const textDiv = document.createElement('div');
429
+ textDiv.className = 'original-text';
430
+ textDiv.innerHTML = `<strong>Review:</strong> "${result.text}"`;
431
+ card.appendChild(textDiv);
432
+
433
+ // Categories section
434
+ const categoriesDiv = document.createElement('div');
435
+ categoriesDiv.className = 'categories';
436
+
437
+ if (result.categories.length > 0) {
438
+ result.categories.forEach(cat => {
439
+ const tag = document.createElement('span');
440
+ tag.className = 'category-tag';
441
+ tag.textContent = cat;
442
+ categoriesDiv.appendChild(tag);
443
+ });
444
+ } else {
445
+ const noCategories = document.createElement('div');
446
+ noCategories.className = 'no-categories';
447
+ noCategories.textContent = 'No categories above threshold';
448
+ categoriesDiv.appendChild(noCategories);
449
+ }
450
+ card.appendChild(categoriesDiv);
451
+
452
+ results.appendChild(card);
453
+ results.style.display = 'block';
454
+ }
455
+ </script>
456
+ </body>
457
+ </html>
458
+ """
459
+
460
+ @app.route('/')
461
+ def index():
462
+ return render_template_string(
463
+ HTML_TEMPLATE,
464
+ model_loaded=MODEL_LOADED,
465
+ labels=labels,
466
+ sample_text=""
467
+ )
468
+
469
+ @app.route('/predict', methods=['POST'])
470
+ def predict_route():
471
+ if not MODEL_LOADED:
472
+ return jsonify({'error': 'Model not loaded. Please check model files.'}), 500
473
+
474
+ try:
475
+ review_text = request.form.get('review_text', '').strip()
476
+
477
+ if not review_text:
478
+ return jsonify({'error': 'Please enter a review.'}), 400
479
+
480
+ # Predict for SINGLE review only
481
+ categories, _ = predict_single(review_text, threshold=0.5)
482
+
483
+ # Format result (only one result object)
484
+ result = {
485
+ 'text': review_text,
486
+ 'categories': categories
487
+ }
488
+
489
+ return jsonify({
490
+ 'success': True,
491
+ 'results': [result], # Still wrapped in list for frontend compatibility
492
+ 'threshold': 0.5
493
+ })
494
+
495
+ except Exception as e:
496
+ return jsonify({'error': f'Prediction error: {str(e)}'}), 500
497
+
498
+ @app.route('/health')
499
+ def health():
500
+ return jsonify({
501
+ 'status': 'healthy',
502
+ 'model_loaded': MODEL_LOADED,
503
+ 'device': DEVICE if MODEL_LOADED else 'N/A',
504
+ 'labels_count': len(labels) if labels else 0
505
+ })
506
+
507
+ if __name__ == '__main__':
508
+ print("="*50)
509
+ print("πŸš€ Starting Fintech Review Classification App")
510
+ print("="*50)
511
+ if MODEL_LOADED:
512
+ print(f"βœ… Model loaded successfully on {DEVICE}")
513
+ print(f"πŸ“‹ Available categories: {len(labels)}")
514
+ print(f"🏷️ Categories: {', '.join(labels[:5])}{'...' if len(labels) > 5 else ''}")
515
+ else:
516
+ print("❌ Model failed to load - app will run in demo mode")
517
+ print("🌐 Open your browser to: http://localhost:5000")
518
+ print("="*50)
519
+
520
+ app.run(host='0.0.0.0', port=5000)