| | import os
|
| | import json
|
| | import joblib
|
| | import torch
|
| | import numpy as np
|
| | from flask import Flask, request, render_template_string, jsonify
|
| | from transformers import AutoTokenizer, AutoModelForSequenceClassification
|
| |
|
| | app = Flask(__name__)
|
| |
|
| |
|
| |
|
| |
|
| | SAVE_DIR = "./model"
|
| |
|
| | try:
|
| |
|
| | tokenizer = AutoTokenizer.from_pretrained(SAVE_DIR)
|
| | model = AutoModelForSequenceClassification.from_pretrained(SAVE_DIR)
|
| | model.eval()
|
| | DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| | model.to(DEVICE)
|
| |
|
| |
|
| | mlb = joblib.load(os.path.join(SAVE_DIR, "mlb.joblib"))
|
| | with open(os.path.join(SAVE_DIR, "labels.json"), "r", encoding="utf-8") as f:
|
| | labels = json.load(f)
|
| |
|
| | MODEL_LOADED = True
|
| | print(f"Model loaded successfully on device: {DEVICE}")
|
| | print(f"Available labels: {labels}")
|
| |
|
| | except Exception as e:
|
| | MODEL_LOADED = False
|
| | print(f"Error loading model: {e}")
|
| | tokenizer = None
|
| | model = None
|
| | mlb = None
|
| | labels = []
|
| |
|
| |
|
| | def sigmoid(x):
|
| | return 1 / (1 + np.exp(-x))
|
| |
|
| |
|
| |
|
| |
|
| | def predict_single(text, threshold=0.5):
|
| | """Predict categories for a single text."""
|
| | if not MODEL_LOADED:
|
| | return [], []
|
| |
|
| |
|
| | encodings = tokenizer(
|
| | [text],
|
| | truncation=True,
|
| | padding=True,
|
| | max_length=256,
|
| | return_tensors="pt"
|
| | ).to(DEVICE)
|
| |
|
| |
|
| | with torch.no_grad():
|
| | outputs = model(**encodings)
|
| | logits = outputs.logits.cpu().numpy()
|
| |
|
| |
|
| | probs = sigmoid(logits)
|
| |
|
| |
|
| | pred_bin = (probs >= threshold).astype(int)
|
| |
|
| |
|
| | row_2d = np.array([pred_bin[0]])
|
| | categories = mlb.inverse_transform(row_2d)[0]
|
| |
|
| | return list(categories), probs[0]
|
| |
|
| |
|
| | HTML_TEMPLATE = """
|
| | <!DOCTYPE html>
|
| | <html lang="en">
|
| | <head>
|
| | <meta charset="UTF-8">
|
| | <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| | <title>Fintech Review Category Classifier</title>
|
| | <style>
|
| | * {
|
| | margin: 0;
|
| | padding: 0;
|
| | box-sizing: border-box;
|
| | }
|
| |
|
| | body {
|
| | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| | background: linear-gradient(135deg, #6ee7b7 0%, #7dd3fc 100%);
|
| | min-height: 100vh;
|
| | padding: 20px;
|
| | display: flex;
|
| | flex-direction: column;
|
| | }
|
| |
|
| | .container {
|
| | max-width: 1000px;
|
| | margin: 0 auto;
|
| | background: rgba(255, 255, 255, 0.95);
|
| | border-radius: 20px;
|
| | box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
| | backdrop-filter: blur(10px);
|
| | overflow: hidden;
|
| | flex: 1;
|
| | }
|
| |
|
| | .header {
|
| | background: linear-gradient(45deg, #10b981, #06b6d4);
|
| | color: white;
|
| | padding: 30px;
|
| | text-align: center;
|
| | }
|
| |
|
| | .header h1 {
|
| | font-size: 2.5em;
|
| | margin-bottom: 10px;
|
| | text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
| | }
|
| |
|
| | .header p {
|
| | font-size: 1.2em;
|
| | opacity: 0.9;
|
| | }
|
| |
|
| | .main-content {
|
| | padding: 40px;
|
| | }
|
| |
|
| | .input-section {
|
| | margin-bottom: 30px;
|
| | }
|
| |
|
| | .form-group {
|
| | margin-bottom: 20px;
|
| | }
|
| |
|
| | label {
|
| | display: block;
|
| | margin-bottom: 10px;
|
| | font-weight: 600;
|
| | color: #333;
|
| | font-size: 1.1em;
|
| | }
|
| |
|
| | textarea {
|
| | width: 100%;
|
| | min-height: 120px;
|
| | padding: 15px;
|
| | border: 2px solid #e0e0e0;
|
| | border-radius: 10px;
|
| | font-size: 16px;
|
| | font-family: inherit;
|
| | resize: vertical;
|
| | transition: all 0.3s ease;
|
| | }
|
| |
|
| | textarea:focus {
|
| | border-color: #10b981;
|
| | outline: none;
|
| | box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
|
| | }
|
| |
|
| | .controls {
|
| | display: flex;
|
| | gap: 20px;
|
| | align-items: center;
|
| | flex-wrap: wrap;
|
| | margin-bottom: 20px;
|
| | }
|
| |
|
| | .btn {
|
| | background: linear-gradient(45deg, #10b981, #06b6d4);
|
| | color: white;
|
| | border: none;
|
| | padding: 15px 30px;
|
| | font-size: 16px;
|
| | font-weight: 600;
|
| | border-radius: 25px;
|
| | cursor: pointer;
|
| | transition: all 0.3s ease;
|
| | box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
|
| | }
|
| |
|
| | .btn:hover {
|
| | transform: translateY(-2px);
|
| | box-shadow: 0 6px 20px rgba(16, 185, 129, 0.4);
|
| | }
|
| |
|
| | .btn:active {
|
| | transform: translateY(0);
|
| | }
|
| |
|
| | .btn:disabled {
|
| | opacity: 0.6;
|
| | cursor: not-allowed;
|
| | transform: none;
|
| | }
|
| |
|
| | .results-section {
|
| | margin-top: 30px;
|
| | }
|
| |
|
| | .result-card {
|
| | background: #f8f9ff;
|
| | border: 1px solid #e0e8ff;
|
| | border-radius: 15px;
|
| | padding: 25px;
|
| | margin-bottom: 20px;
|
| | box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
|
| | }
|
| |
|
| | .categories {
|
| | display: flex;
|
| | flex-wrap: wrap;
|
| | gap: 10px;
|
| | margin-bottom: 15px;
|
| | }
|
| |
|
| | .category-tag {
|
| | background: linear-gradient(45deg, #48bb78, #38a169);
|
| | color: white;
|
| | padding: 8px 15px;
|
| | border-radius: 20px;
|
| | font-size: 14px;
|
| | font-weight: 500;
|
| | box-shadow: 0 2px 5px rgba(72, 187, 120, 0.3);
|
| | }
|
| |
|
| | .no-categories {
|
| | color: #666;
|
| | font-style: italic;
|
| | padding: 10px;
|
| | background: #f0f0f0;
|
| | border-radius: 8px;
|
| | }
|
| |
|
| | .loading {
|
| | display: none;
|
| | text-align: center;
|
| | padding: 20px;
|
| | color: #10b981;
|
| | font-size: 18px;
|
| | }
|
| |
|
| | .loading.show {
|
| | display: block;
|
| | }
|
| |
|
| | .error {
|
| | background: #fed7d7;
|
| | color: #c53030;
|
| | padding: 15px;
|
| | border-radius: 8px;
|
| | margin: 20px 0;
|
| | border-left: 4px solid #c53030;
|
| | }
|
| |
|
| | .model-status {
|
| | padding: 15px;
|
| | border-radius: 8px;
|
| | margin-bottom: 20px;
|
| | font-weight: 500;
|
| | }
|
| |
|
| | .model-status.loaded {
|
| | background: #c6f6d5;
|
| | color: #22543d;
|
| | border-left: 4px solid #38a169;
|
| | }
|
| |
|
| | .model-status.error {
|
| | background: #fed7d7;
|
| | color: #c53030;
|
| | border-left: 4px solid #c53030;
|
| | }
|
| |
|
| | footer {
|
| | text-align: center;
|
| | padding: 20px;
|
| | background: #059669;
|
| | color: #ecf0f1;
|
| | font-size: 14px;
|
| | margin-top: auto;
|
| | }
|
| |
|
| | footer a {
|
| | color: #6ee7b7;
|
| | text-decoration: none;
|
| | font-weight: 600;
|
| | }
|
| |
|
| | footer a:hover {
|
| | text-decoration: underline;
|
| | }
|
| |
|
| | @media (max-width: 768px) {
|
| | .header h1 {
|
| | font-size: 2em;
|
| | }
|
| |
|
| | .main-content {
|
| | padding: 20px;
|
| | }
|
| |
|
| | .controls {
|
| | flex-direction: column;
|
| | align-items: stretch;
|
| | }
|
| | }
|
| | </style>
|
| | </head>
|
| | <body>
|
| | <div class="container">
|
| | <div class="header">
|
| | <h1>Fintech Review Classifier</h1>
|
| | <p>Classify your customer review into relevant categories</p>
|
| | </div>
|
| |
|
| | <div class="main-content">
|
| | {% if model_loaded %}
|
| | <div class="model-status loaded">
|
| | Model loaded successfully! Available categories: {{ labels|length }}
|
| | </div>
|
| | {% else %}
|
| | <div class="model-status error">
|
| | β Model could not be loaded. Please check if the model files exist in './model' directory.
|
| | </div>
|
| | {% endif %}
|
| |
|
| | <form id="classifyForm" {% if not model_loaded %}style="opacity: 0.5; pointer-events: none;"{% endif %}>
|
| | <div class="input-section">
|
| | <div class="form-group">
|
| | <label for="review_text">Enter Customer Review:</label>
|
| | <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>
|
| | </div>
|
| |
|
| | <div class="controls">
|
| | <button type="submit" class="btn" {% if not model_loaded %}disabled{% endif %}>
|
| | π Classify Review
|
| | </button>
|
| | </div>
|
| | </div>
|
| | </form>
|
| |
|
| | <div class="loading" id="loading">
|
| | <div>π€ Analyzing review...</div>
|
| | </div>
|
| |
|
| | <div class="results-section" id="results" style="display: none;">
|
| | <!-- Results will be inserted here -->
|
| | </div>
|
| | </div>
|
| | </div>
|
| |
|
| | <footer>
|
| | Made with β€οΈ by Adediran Adeyemi β <a href="https://www.linkedin.com/in/adediran-adeyemi-17103b114/" target="_blank">Connect with me on LinkedIn</a>
|
| | </footer>
|
| |
|
| | <script>
|
| | // Handle form submission
|
| | document.getElementById('classifyForm').addEventListener('submit', async function(e) {
|
| | e.preventDefault();
|
| |
|
| | const formData = new FormData(this);
|
| | const loading = document.getElementById('loading');
|
| | const results = document.getElementById('results');
|
| |
|
| | // Show loading, hide results
|
| | loading.classList.add('show');
|
| | results.style.display = 'none';
|
| |
|
| | try {
|
| | const response = await fetch('/predict', {
|
| | method: 'POST',
|
| | body: formData
|
| | });
|
| |
|
| | const data = await response.json();
|
| |
|
| | if (data.error) {
|
| | throw new Error(data.error);
|
| | }
|
| |
|
| | displayResults(data);
|
| |
|
| | } catch (error) {
|
| | results.innerHTML = '<div class="error">β Error: ' + error.message + '</div>';
|
| | results.style.display = 'block';
|
| | } finally {
|
| | loading.classList.remove('show');
|
| | }
|
| | });
|
| |
|
| | function displayResults(data) {
|
| | const results = document.getElementById('results');
|
| |
|
| | // Clear any existing content completely
|
| | results.innerHTML = '';
|
| |
|
| | // Create results header
|
| | const header = document.createElement('h2');
|
| | header.textContent = 'π― Classification Result';
|
| | results.appendChild(header);
|
| |
|
| | // Only one result expected
|
| | const result = data.results[0];
|
| | const card = document.createElement('div');
|
| | card.className = 'result-card';
|
| |
|
| | // Categories section (no review text displayed)
|
| | const categoriesDiv = document.createElement('div');
|
| | categoriesDiv.className = 'categories';
|
| |
|
| | if (result.categories.length > 0) {
|
| | result.categories.forEach(cat => {
|
| | const tag = document.createElement('span');
|
| | tag.className = 'category-tag';
|
| | tag.textContent = cat;
|
| | categoriesDiv.appendChild(tag);
|
| | });
|
| | } else {
|
| | const noCategories = document.createElement('div');
|
| | noCategories.className = 'no-categories';
|
| | noCategories.textContent = 'No categories above threshold';
|
| | categoriesDiv.appendChild(noCategories);
|
| | }
|
| | card.appendChild(categoriesDiv);
|
| |
|
| | results.appendChild(card);
|
| | results.style.display = 'block';
|
| | }
|
| | </script>
|
| | </body>
|
| | </html>
|
| | """
|
| |
|
| | @app.route('/')
|
| | def index():
|
| | return render_template_string(
|
| | HTML_TEMPLATE,
|
| | model_loaded=MODEL_LOADED,
|
| | labels=labels,
|
| | sample_text=""
|
| | )
|
| |
|
| | @app.route('/predict', methods=['POST'])
|
| | def predict_route():
|
| | if not MODEL_LOADED:
|
| | return jsonify({'error': 'Model not loaded. Please check model files.'}), 500
|
| |
|
| | try:
|
| | review_text = request.form.get('review_text', '').strip()
|
| |
|
| | if not review_text:
|
| | return jsonify({'error': 'Please enter a review.'}), 400
|
| |
|
| |
|
| | categories, _ = predict_single(review_text, threshold=0.5)
|
| |
|
| |
|
| | result = {
|
| | 'text': review_text,
|
| | 'categories': categories
|
| | }
|
| |
|
| | return jsonify({
|
| | 'success': True,
|
| | 'results': [result],
|
| | 'threshold': 0.5
|
| | })
|
| |
|
| | except Exception as e:
|
| | return jsonify({'error': f'Prediction error: {str(e)}'}), 500
|
| |
|
| | @app.route('/health')
|
| | def health():
|
| | return jsonify({
|
| | 'status': 'healthy',
|
| | 'model_loaded': MODEL_LOADED,
|
| | 'device': DEVICE if MODEL_LOADED else 'N/A',
|
| | 'labels_count': len(labels) if labels else 0
|
| | })
|
| |
|
| | if __name__ == '__main__':
|
| | print("="*50)
|
| | print("π Starting Fintech Review Classification App")
|
| | print("="*50)
|
| | if MODEL_LOADED:
|
| | print(f"Model loaded successfully on {DEVICE}")
|
| | print(f"Available categories: {len(labels)}")
|
| | print(f"Categories: {', '.join(labels[:5])}{'...' if len(labels) > 5 else ''}")
|
| | else:
|
| | print("β Model failed to load - app will run in demo mode")
|
| |
|
| |
|
| | port = int(os.environ.get("PORT", 7860))
|
| | print(f"π Starting server on port: {port}")
|
| | print("="*50)
|
| |
|
| | app.run(host='0.0.0.0', port=port) |