AOUNZakaria commited on
Commit
77b0c9c
·
verified ·
1 Parent(s): 4c32453

Upload 10 files

Browse files
Files changed (10) hide show
  1. Dockerfile +24 -0
  2. Models/Text_LR.pkl +3 -0
  3. Models/count_vect.pkl +3 -0
  4. Models/transformer.pkl +3 -0
  5. app.py +76 -0
  6. main.py +8 -0
  7. models.py +89 -0
  8. pyproject.toml +15 -0
  9. templates/index.html +128 -0
  10. utils.py +38 -0
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.11-slim
3
+
4
+ # Set environment variables
5
+ ENV PYTHONDONTWRITEBYTECODE 1
6
+ ENV PYTHONUNBUFFERED 1
7
+
8
+ # Set working directory
9
+ WORKDIR /app
10
+
11
+ # Install dependencies
12
+ RUN pip install --no-cache-dir flask flask-cors flask-sqlalchemy gunicorn numpy scikit-learn
13
+
14
+ # Copy the application code
15
+ COPY . /app/
16
+
17
+ # Create Models directory
18
+ RUN mkdir -p /app/Models
19
+
20
+ # Expose port 5000 for the Flask app
21
+ EXPOSE 5000
22
+
23
+ # Command to run the application using gunicorn
24
+ CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--reload", "main:app"]
Models/Text_LR.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:450f850036a2d38ffa16fdea1c215f84b53aaa0891cf1456324598be7f73d640
3
+ size 2070402
Models/count_vect.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:86909c3f6eaf2f7b0cb9eb73f643a633348343acc9c45ac51472e6c6f06b11c6
3
+ size 1378074
Models/transformer.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a5a01a4e3c4c9f583d8085382e7075939baf09c20ff23a1c27ef20fa8a6b164b
3
+ size 690215
app.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from flask import Flask, request, jsonify, render_template
3
+ from flask_cors import CORS
4
+ import logging
5
+ from models import SentimentModel
6
+ from utils import validate_input, setup_logging
7
+
8
+ # Initialize Flask app
9
+ app = Flask(__name__)
10
+ CORS(app) # Enable CORS for all routes
11
+ app.secret_key = os.environ.get("SESSION_SECRET", "default-secret-key")
12
+
13
+ # Setup logging
14
+ setup_logging()
15
+
16
+ # Initialize the sentiment model
17
+ sentiment_model = SentimentModel()
18
+
19
+ @app.errorhandler(Exception)
20
+ def handle_error(error):
21
+ """Global error handler for all exceptions."""
22
+ logging.error(f"Error occurred: {str(error)}")
23
+
24
+ if isinstance(error, ValueError):
25
+ return jsonify({"error": str(error)}), 400
26
+
27
+ return jsonify({
28
+ "error": "An internal error occurred. Please try again later."
29
+ }), 500
30
+
31
+ @app.route('/')
32
+ def index():
33
+ """Render the main application page."""
34
+ return render_template('index.html')
35
+
36
+ @app.route('/health', methods=['GET'])
37
+ def health_check():
38
+ """Health check endpoint."""
39
+ return jsonify({"status": "healthy"}), 200
40
+
41
+ @app.route('/predict', methods=['POST'])
42
+ def predict_sentiment():
43
+ """
44
+ Endpoint for sentiment prediction.
45
+
46
+ Expects JSON input with format:
47
+ {
48
+ "text": "text to analyze"
49
+ }
50
+
51
+ Returns:
52
+ {
53
+ "sentiment": "positive/negative",
54
+ "confidence": float
55
+ }
56
+ """
57
+ try:
58
+ # Get and validate input
59
+ data = request.get_json()
60
+ if not data:
61
+ raise ValueError("No input data provided")
62
+
63
+ text = validate_input(data)
64
+
65
+ # Get prediction
66
+ sentiment, confidence = sentiment_model.predict(text)
67
+
68
+ # Return response
69
+ return jsonify({
70
+ "sentiment": sentiment,
71
+ "confidence": confidence
72
+ }), 200
73
+
74
+ except Exception as e:
75
+ # Let the global error handler deal with it
76
+ raise
main.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from app import app
2
+ import logging
3
+ from utils import setup_logging
4
+
5
+ if __name__ == "__main__":
6
+ setup_logging()
7
+ logging.info("Starting sentiment analysis API server")
8
+ app.run(host="0.0.0.0", port=5000, debug=True)
models.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pickle
2
+ import logging
3
+ import os
4
+ from typing import Tuple, Any
5
+ from pathlib import Path
6
+
7
+ class SentimentModel:
8
+ def __init__(self):
9
+ self.count_vectorizer = None
10
+ self.tfidf_transformer = None
11
+ self.classifier = None
12
+ self._load_models()
13
+
14
+ def _load_models(self) -> None:
15
+ """Load all required ML models from pickle files."""
16
+ try:
17
+ # Get model path from environment or use default relative path
18
+ default_path = str(Path(__file__).parent / 'Models')
19
+ model_path = os.getenv('MODEL_PATH', default_path)
20
+ logging.info(f"Loading models from: {model_path}")
21
+
22
+ # Ensure the directory exists
23
+ if not os.path.exists(model_path):
24
+ raise FileNotFoundError(f"Model directory not found at: {model_path}")
25
+
26
+ model_files = {
27
+ 'count_vectorizer': 'count_vect.pkl',
28
+ 'tfidf_transformer': 'transformer.pkl',
29
+ 'classifier': 'Text_LR.pkl'
30
+ }
31
+
32
+ for model_name, filename in model_files.items():
33
+ file_path = os.path.join(model_path, filename)
34
+ if not os.path.exists(file_path):
35
+ raise FileNotFoundError(f"Model file not found: {file_path}")
36
+
37
+ with open(file_path, 'rb') as f:
38
+ setattr(self, model_name, pickle.load(f))
39
+ logging.info(f"Successfully loaded {model_name}")
40
+
41
+ except FileNotFoundError as e:
42
+ logging.error(f"Model file not found: {str(e)}")
43
+ raise
44
+ except Exception as e:
45
+ logging.error(f"Error loading models: {str(e)}")
46
+ raise
47
+
48
+ def predict(self, text: str) -> Tuple[str, float]:
49
+ """
50
+ Predict sentiment for given text using the ML pipeline.
51
+
52
+ Args:
53
+ text: Input text for sentiment analysis
54
+
55
+ Returns:
56
+ Tuple containing sentiment label and confidence score
57
+ """
58
+ try:
59
+ if not all([self.count_vectorizer, self.tfidf_transformer, self.classifier]):
60
+ raise RuntimeError("Models not properly initialized")
61
+
62
+ # Transform text using CountVectorizer
63
+ count_features = self.count_vectorizer.transform([text])
64
+ logging.debug(f"Count features shape: {count_features.shape}")
65
+
66
+ # Apply TF-IDF transformation
67
+ tfidf_features = self.tfidf_transformer.transform(count_features)
68
+ logging.debug(f"TF-IDF features shape: {tfidf_features.shape}")
69
+
70
+ # Get prediction probabilities
71
+ probabilities = self.classifier.predict_proba(tfidf_features)[0]
72
+ logging.debug(f"Raw prediction probabilities: {probabilities}")
73
+
74
+ # Find the class with highest probability
75
+ max_prob_idx = probabilities.argmax()
76
+ confidence = probabilities[max_prob_idx]
77
+
78
+ # Map the prediction index to sentiment
79
+ # Class 2 (index 2) appears to be positive sentiment based on the logs
80
+ sentiment = "positive" if max_prob_idx == 2 else "negative"
81
+
82
+ logging.info(f"Prediction for text: '{text[:50]}...' -> {sentiment} (confidence: {confidence:.2f})")
83
+ logging.debug(f"Probabilities - Positive: {confidence:.3f}")
84
+
85
+ return sentiment, float(confidence)
86
+
87
+ except Exception as e:
88
+ logging.error(f"Prediction error: {str(e)}")
89
+ raise
pyproject.toml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "repl-nix-workspace"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ requires-python = ">=3.11"
6
+ dependencies = [
7
+ "email-validator>=2.2.0",
8
+ "flask-cors>=5.0.1",
9
+ "flask>=3.1.0",
10
+ "flask-sqlalchemy>=3.1.1",
11
+ "gunicorn>=23.0.0",
12
+ "numpy>=2.2.4",
13
+ "psycopg2-binary>=2.9.10",
14
+ "scikit-learn>=1.6.1",
15
+ ]
templates/index.html ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-bs-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Sentiment Analysis</title>
7
+ <link href="https://cdn.replit.com/agent/bootstrap-agent-dark-theme.min.css" rel="stylesheet">
8
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet">
9
+ </head>
10
+ <body>
11
+ <div class="container py-5">
12
+ <div class="row justify-content-center">
13
+ <div class="col-md-8">
14
+ <div class="card shadow-sm">
15
+ <div class="card-body">
16
+ <h1 class="card-title text-center mb-4">
17
+ <i class="bi bi-emoji-smile me-2"></i>
18
+ Sentiment Analysis
19
+ </h1>
20
+
21
+ <div class="alert alert-info mb-4" role="alert">
22
+ <i class="bi bi-info-circle me-2"></i>
23
+ Enter your text below to analyze its sentiment. Our AI model will determine if the text expresses a positive or negative sentiment.
24
+ </div>
25
+
26
+ <form id="sentimentForm" class="mb-4">
27
+ <div class="mb-3">
28
+ <label for="textInput" class="form-label">Text to Analyze</label>
29
+ <textarea
30
+ class="form-control"
31
+ id="textInput"
32
+ rows="4"
33
+ placeholder="Enter your text here..."
34
+ required></textarea>
35
+ </div>
36
+ <div class="d-grid">
37
+ <button type="submit" class="btn btn-primary" id="analyzeBtn">
38
+ <span class="spinner-border spinner-border-sm d-none me-2" role="status" aria-hidden="true"></span>
39
+ Analyze Sentiment
40
+ </button>
41
+ </div>
42
+ </form>
43
+
44
+ <div id="result" class="card d-none">
45
+ <div class="card-body text-center">
46
+ <h5 class="card-title mb-3">Analysis Result</h5>
47
+ <div class="result-content">
48
+ <i class="bi bi-emoji-smile-fill result-icon fs-1 mb-3"></i>
49
+ <p class="result-text fs-4 mb-0"></p>
50
+ <p class="confidence-text text-muted mt-2"></p>
51
+ </div>
52
+ </div>
53
+ </div>
54
+
55
+ <div id="errorAlert" class="alert alert-danger d-none" role="alert">
56
+ <i class="bi bi-exclamation-triangle me-2"></i>
57
+ <span class="error-message"></span>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </div>
64
+
65
+ <script>
66
+ document.addEventListener('DOMContentLoaded', () => {
67
+ const form = document.getElementById('sentimentForm');
68
+ const analyzeBtn = document.getElementById('analyzeBtn');
69
+ const spinner = analyzeBtn.querySelector('.spinner-border');
70
+ const resultCard = document.getElementById('result');
71
+ const resultIcon = resultCard.querySelector('.result-icon');
72
+ const resultText = resultCard.querySelector('.result-text');
73
+ const confidenceText = resultCard.querySelector('.confidence-text');
74
+ const errorAlert = document.getElementById('errorAlert');
75
+ const errorMessage = errorAlert.querySelector('.error-message');
76
+
77
+ form.addEventListener('submit', async (e) => {
78
+ e.preventDefault();
79
+
80
+ // Reset previous results
81
+ resultCard.classList.add('d-none');
82
+ errorAlert.classList.add('d-none');
83
+
84
+ // Show loading state
85
+ analyzeBtn.disabled = true;
86
+ spinner.classList.remove('d-none');
87
+
88
+ try {
89
+ const text = document.getElementById('textInput').value.trim();
90
+
91
+ if (!text) {
92
+ throw new Error('Please enter some text to analyze.');
93
+ }
94
+
95
+ const response = await fetch('/predict', {
96
+ method: 'POST',
97
+ headers: {
98
+ 'Content-Type': 'application/json',
99
+ },
100
+ body: JSON.stringify({ text }),
101
+ });
102
+
103
+ if (!response.ok) {
104
+ const error = await response.json();
105
+ throw new Error(error.error || 'Failed to analyze sentiment.');
106
+ }
107
+
108
+ const result = await response.json();
109
+
110
+ // Update result display
111
+ resultIcon.className = `bi ${result.sentiment === 'positive' ? 'bi-emoji-smile-fill' : 'bi-emoji-frown-fill'} result-icon fs-1 mb-3`;
112
+ resultText.textContent = `${result.sentiment.charAt(0).toUpperCase() + result.sentiment.slice(1)} Sentiment`;
113
+ confidenceText.textContent = `Confidence: ${Math.round(result.confidence * 100)}%`;
114
+ resultCard.classList.remove('d-none');
115
+
116
+ } catch (error) {
117
+ errorMessage.textContent = error.message;
118
+ errorAlert.classList.remove('d-none');
119
+ } finally {
120
+ // Reset loading state
121
+ analyzeBtn.disabled = false;
122
+ spinner.classList.add('d-none');
123
+ }
124
+ });
125
+ });
126
+ </script>
127
+ </body>
128
+ </html>
utils.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Any
2
+ import logging
3
+
4
+ def validate_input(data: Dict[str, Any]) -> str:
5
+ """
6
+ Validate the input data for sentiment analysis.
7
+
8
+ Args:
9
+ data: Dictionary containing the input data
10
+
11
+ Returns:
12
+ Validated text string
13
+
14
+ Raises:
15
+ ValueError: If validation fails
16
+ """
17
+ if not isinstance(data, dict):
18
+ raise ValueError("Input must be a JSON object")
19
+
20
+ if 'text' not in data:
21
+ raise ValueError("Missing 'text' field in input")
22
+
23
+ text = data.get('text')
24
+
25
+ if not isinstance(text, str):
26
+ raise ValueError("Text must be a string")
27
+
28
+ if not text.strip():
29
+ raise ValueError("Text cannot be empty")
30
+
31
+ return text.strip()
32
+
33
+ def setup_logging() -> None:
34
+ """Configure logging for the application."""
35
+ logging.basicConfig(
36
+ level=logging.DEBUG,
37
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
38
+ )