SakibAhmed commited on
Commit
80c4760
·
verified ·
1 Parent(s): 761c2ff

Upload 7 files

Browse files
Files changed (7) hide show
  1. .env +1 -0
  2. Dockerfile +29 -0
  3. app.py +121 -0
  4. models/best_88E.pt +3 -0
  5. postman.json +44 -0
  6. requirements.txt +5 -0
  7. templates/index.html +517 -0
.env ADDED
@@ -0,0 +1 @@
 
 
1
+ MODEL_NAME=best_88E.pt
Dockerfile ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.10-slim
3
+
4
+ # Set the working directory in the container
5
+ WORKDIR /app
6
+
7
+ # Copy the requirements file
8
+ COPY requirements.txt requirements.txt
9
+
10
+ # Install Python packages
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+ # Copy application code
14
+ COPY . /app
15
+
16
+ # Create a non-root user
17
+ RUN useradd -m -u 1000 user
18
+
19
+ # Change ownership
20
+ RUN chown -R user:user /app
21
+
22
+ # Switch to the non-root user
23
+ USER user
24
+
25
+ # Expose the port Gunicorn will run on (Using 7860 as in CMD)
26
+ EXPOSE 7860
27
+
28
+ # Command to run the app
29
+ CMD ["python", "app.py", "--host", "0.0.0.0", "--port", "7860"]
app.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+
3
+ import os
4
+ import torch
5
+ from flask import Flask, request, jsonify, render_template
6
+ from flask_cors import CORS
7
+ from werkzeug.utils import secure_filename
8
+ from ultralytics import YOLO
9
+ from dotenv import load_dotenv
10
+
11
+ # Load environment variables from .env file
12
+ load_dotenv()
13
+
14
+ app = Flask(__name__)
15
+
16
+ # Enable CORS for all routes
17
+ CORS(app)
18
+
19
+ # --- Configuration ---
20
+ UPLOAD_FOLDER = 'static/uploads'
21
+ MODELS_FOLDER = 'models' # New folder for models
22
+ ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg'}
23
+
24
+ # Load model name from .env file, with a fallback default
25
+ MODEL_NAME = os.getenv('MODEL_NAME', 'best.pt')
26
+ MODEL_PATH = os.path.join(MODELS_FOLDER, MODEL_NAME)
27
+
28
+ app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
29
+ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
30
+ os.makedirs(MODELS_FOLDER, exist_ok=True) # Ensure models folder exists
31
+ os.makedirs('templates', exist_ok=True) # Ensure templates folder exists
32
+
33
+ # --- Determine Device and Load YOLO Model ---
34
+ # Use CUDA if available, otherwise use CPU
35
+ device = "cuda" if torch.cuda.is_available() else "cpu"
36
+ print(f"Using device: {device}")
37
+
38
+ # Load the model once when the application starts for efficiency.
39
+ model = None
40
+ try:
41
+ if not os.path.exists(MODEL_PATH):
42
+ print(f"Error: Model file not found at {MODEL_PATH}")
43
+ print("Please make sure the model file exists and the MODEL_NAME in your .env file is correct.")
44
+ else:
45
+ model = YOLO(MODEL_PATH)
46
+ model.to(device) # Move model to the selected device
47
+ print(f"Successfully loaded model '{MODEL_NAME}' on {device}.")
48
+ except Exception as e:
49
+ print(f"Error loading YOLO model: {e}")
50
+
51
+ def allowed_file(filename):
52
+ """Checks if a file's extension is in the ALLOWED_EXTENSIONS set."""
53
+ return '.' in filename and \
54
+ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
55
+
56
+ @app.route('/')
57
+ def home():
58
+ """Serve the main HTML page."""
59
+ return render_template('index.html')
60
+
61
+ @app.route('/predict', methods=['POST'])
62
+ def predict():
63
+ """
64
+ Endpoint to receive an image, run YOLO classification, and return the single best prediction.
65
+ """
66
+ if model is None:
67
+ return jsonify({"error": "Model could not be loaded. Please check server logs."}), 500
68
+
69
+ # 1. --- File Validation ---
70
+ if 'file' not in request.files:
71
+ return jsonify({"error": "No file part in the request"}), 400
72
+
73
+ file = request.files['file']
74
+ if file.filename == '':
75
+ return jsonify({"error": "No selected file"}), 400
76
+
77
+ if not file or not allowed_file(file.filename):
78
+ return jsonify({"error": "File type not allowed"}), 400
79
+
80
+ # 2. --- Save the File Temporarily ---
81
+ filename = secure_filename(file.filename)
82
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
83
+ file.save(filepath)
84
+
85
+ # 3. --- Perform Inference ---
86
+ try:
87
+ # Run the YOLO model on the uploaded image. The model is already on the correct device.
88
+ results = model(filepath)
89
+
90
+ # 4. --- Process Results to Get ONLY the Top Prediction ---
91
+ # Get the first result object from the list
92
+ result = results[0]
93
+
94
+ # Access the probabilities object
95
+ probs = result.probs
96
+
97
+ # Get the index and confidence of the top prediction
98
+ top1_index = probs.top1
99
+ top1_confidence = float(probs.top1conf) # Convert tensor to Python float
100
+
101
+ # Get the class name from the model's 'names' dictionary
102
+ class_name = model.names[top1_index]
103
+
104
+ # Create the final prediction object
105
+ prediction = {
106
+ "class": class_name,
107
+ "confidence": top1_confidence
108
+ }
109
+
110
+ # Return the single prediction object as JSON
111
+ return jsonify(prediction)
112
+
113
+ except Exception as e:
114
+ return jsonify({"error": f"An error occurred during inference: {str(e)}"}), 500
115
+ finally:
116
+ # 5. --- Cleanup ---
117
+ if os.path.exists(filepath):
118
+ os.remove(filepath)
119
+
120
+ if __name__ == '__main__':
121
+ app.run(host='0.0.0.0', port=5000, debug=True)
models/best_88E.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:05e883e1d97b5a9c472c7cc8509a311f504342d49943807ae52623f28a03f114
3
+ size 136962165
postman.json ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "info": {
3
+ "_postman_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
4
+ "name": "NOW Classification API",
5
+ "description": "A collection for testing the Flask API that serves a YOLOv8 classification model. The API takes an image file and returns the top predicted classes with their confidence scores.",
6
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
7
+ },
8
+ "item": [
9
+ {
10
+ "name": "Predict Image Class",
11
+ "request": {
12
+ "method": "POST",
13
+ "header": [],
14
+ "body": {
15
+ "mode": "formdata",
16
+ "formdata": [
17
+ {
18
+ "key": "file",
19
+ "type": "file",
20
+ "description": "The image file (jpg, png, jpeg) to be classified. You must select a file from your computer.",
21
+ "src": []
22
+ }
23
+ ]
24
+ },
25
+ "url": {
26
+ "raw": "http://127.0.0.1:5000/predict",
27
+ "protocol": "http",
28
+ "host": [
29
+ "127",
30
+ "0",
31
+ "0",
32
+ "1"
33
+ ],
34
+ "port": "5000",
35
+ "path": [
36
+ "predict"
37
+ ]
38
+ },
39
+ "description": "Upload an image to get its classification predictions. \n\n**Instructions:**\n1. Go to the **Body** tab below.\n2. Make sure **form-data** is selected.\n3. Find the key named `file`.\n4. On the right side of that row, click **Select Files** and choose an image from your computer.\n5. Click **Send**."
40
+ },
41
+ "response": []
42
+ }
43
+ ]
44
+ }
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ Flask==3.1.1
2
+ python-dotenv==1.1.0
3
+ torch==2.3.1+cu118
4
+ ultralytics==8.3.105
5
+ Werkzeug==3.1.3
templates/index.html ADDED
@@ -0,0 +1,517 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>YOLO Vision AI</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
+ background: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 50%, #16213e 100%);
17
+ min-height: 100vh;
18
+ overflow-x: hidden;
19
+ position: relative;
20
+ }
21
+
22
+ /* Animated background particles */
23
+ .particles {
24
+ position: absolute;
25
+ width: 100%;
26
+ height: 100%;
27
+ overflow: hidden;
28
+ z-index: 0;
29
+ }
30
+
31
+ .particle {
32
+ position: absolute;
33
+ width: 2px;
34
+ height: 2px;
35
+ background: #00d4ff;
36
+ border-radius: 50%;
37
+ animation: float 6s ease-in-out infinite;
38
+ opacity: 0.6;
39
+ }
40
+
41
+ @keyframes float {
42
+ 0%, 100% { transform: translateY(0px) rotate(0deg); }
43
+ 50% { transform: translateY(-20px) rotate(180deg); }
44
+ }
45
+
46
+ .container {
47
+ position: relative;
48
+ z-index: 1;
49
+ max-width: 800px;
50
+ margin: 0 auto;
51
+ padding: 2rem;
52
+ min-height: 100vh;
53
+ display: flex;
54
+ flex-direction: column;
55
+ justify-content: center;
56
+ align-items: center;
57
+ }
58
+
59
+ .header {
60
+ text-align: center;
61
+ margin-bottom: 3rem;
62
+ animation: slideDown 1s ease-out;
63
+ }
64
+
65
+ .title {
66
+ font-size: 3.5rem;
67
+ font-weight: 700;
68
+ background: linear-gradient(45deg, #00d4ff, #ff00ff, #00ff88);
69
+ background-size: 200% 200%;
70
+ -webkit-background-clip: text;
71
+ -webkit-text-fill-color: transparent;
72
+ background-clip: text;
73
+ animation: gradientShift 3s ease-in-out infinite;
74
+ margin-bottom: 1rem;
75
+ text-shadow: 0 0 30px rgba(0, 212, 255, 0.5);
76
+ }
77
+
78
+ .subtitle {
79
+ font-size: 1.2rem;
80
+ color: #a0a0a0;
81
+ font-weight: 300;
82
+ }
83
+
84
+ .upload-area {
85
+ width: 100%;
86
+ max-width: 500px;
87
+ min-height: 300px;
88
+ border: 2px dashed #00d4ff;
89
+ border-radius: 20px;
90
+ background: rgba(0, 212, 255, 0.05);
91
+ backdrop-filter: blur(10px);
92
+ display: flex;
93
+ flex-direction: column;
94
+ justify-content: center;
95
+ align-items: center;
96
+ cursor: pointer;
97
+ transition: all 0.3s ease;
98
+ position: relative;
99
+ overflow: hidden;
100
+ animation: slideUp 1s ease-out 0.3s both;
101
+ }
102
+
103
+ .upload-area:hover {
104
+ border-color: #ff00ff;
105
+ background: rgba(255, 0, 255, 0.05);
106
+ transform: translateY(-5px);
107
+ box-shadow: 0 20px 40px rgba(0, 212, 255, 0.2);
108
+ }
109
+
110
+ .upload-area.dragover {
111
+ border-color: #00ff88;
112
+ background: rgba(0, 255, 136, 0.1);
113
+ transform: scale(1.02);
114
+ }
115
+
116
+ .upload-icon {
117
+ font-size: 4rem;
118
+ color: #00d4ff;
119
+ margin-bottom: 1rem;
120
+ transition: all 0.3s ease;
121
+ }
122
+
123
+ .upload-area:hover .upload-icon {
124
+ color: #ff00ff;
125
+ transform: scale(1.1);
126
+ }
127
+
128
+ .upload-text {
129
+ color: #ffffff;
130
+ font-size: 1.1rem;
131
+ margin-bottom: 0.5rem;
132
+ font-weight: 500;
133
+ }
134
+
135
+ .upload-subtext {
136
+ color: #a0a0a0;
137
+ font-size: 0.9rem;
138
+ }
139
+
140
+ .file-input {
141
+ display: none;
142
+ }
143
+
144
+ .preview-container {
145
+ margin-top: 2rem;
146
+ text-align: center;
147
+ animation: fadeIn 0.5s ease-out;
148
+ }
149
+
150
+ .preview-image {
151
+ max-width: 100%;
152
+ max-height: 300px;
153
+ border-radius: 15px;
154
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
155
+ border: 2px solid #00d4ff;
156
+ }
157
+
158
+ .predict-button {
159
+ background: linear-gradient(45deg, #00d4ff, #0099cc);
160
+ border: none;
161
+ color: white;
162
+ padding: 15px 40px;
163
+ font-size: 1.1rem;
164
+ font-weight: 600;
165
+ border-radius: 50px;
166
+ cursor: pointer;
167
+ margin-top: 1.5rem;
168
+ transition: all 0.3s ease;
169
+ text-transform: uppercase;
170
+ letter-spacing: 1px;
171
+ position: relative;
172
+ overflow: hidden;
173
+ }
174
+
175
+ .predict-button:hover {
176
+ transform: translateY(-2px);
177
+ box-shadow: 0 10px 25px rgba(0, 212, 255, 0.4);
178
+ background: linear-gradient(45deg, #ff00ff, #cc0099);
179
+ }
180
+
181
+ .predict-button:disabled {
182
+ opacity: 0.6;
183
+ cursor: not-allowed;
184
+ transform: none;
185
+ }
186
+
187
+ .loading {
188
+ display: none;
189
+ margin-top: 1rem;
190
+ }
191
+
192
+ .spinner {
193
+ width: 40px;
194
+ height: 40px;
195
+ border: 4px solid rgba(0, 212, 255, 0.3);
196
+ border-top: 4px solid #00d4ff;
197
+ border-radius: 50%;
198
+ animation: spin 1s linear infinite;
199
+ margin: 0 auto;
200
+ }
201
+
202
+ .result-container {
203
+ margin-top: 2rem;
204
+ padding: 2rem;
205
+ background: rgba(255, 255, 255, 0.05);
206
+ backdrop-filter: blur(15px);
207
+ border-radius: 20px;
208
+ border: 1px solid rgba(0, 212, 255, 0.3);
209
+ animation: slideUp 0.5s ease-out;
210
+ text-align: center;
211
+ }
212
+
213
+ .result-class {
214
+ font-size: 2rem;
215
+ font-weight: 700;
216
+ color: #00ff88;
217
+ margin-bottom: 1rem;
218
+ text-transform: uppercase;
219
+ letter-spacing: 2px;
220
+ }
221
+
222
+ .result-confidence {
223
+ font-size: 1.2rem;
224
+ color: #ffffff;
225
+ margin-bottom: 0.5rem;
226
+ }
227
+
228
+ .confidence-bar {
229
+ width: 100%;
230
+ height: 10px;
231
+ background: rgba(255, 255, 255, 0.1);
232
+ border-radius: 5px;
233
+ overflow: hidden;
234
+ margin-top: 1rem;
235
+ }
236
+
237
+ .confidence-fill {
238
+ height: 100%;
239
+ background: linear-gradient(90deg, #00d4ff, #00ff88);
240
+ border-radius: 5px;
241
+ transition: width 1s ease-out;
242
+ animation: pulse 2s ease-in-out infinite;
243
+ }
244
+
245
+ .error {
246
+ color: #ff4444;
247
+ background: rgba(255, 68, 68, 0.1);
248
+ padding: 1rem;
249
+ border-radius: 10px;
250
+ border: 1px solid #ff4444;
251
+ margin-top: 1rem;
252
+ }
253
+
254
+ @keyframes slideDown {
255
+ from { opacity: 0; transform: translateY(-50px); }
256
+ to { opacity: 1; transform: translateY(0); }
257
+ }
258
+
259
+ @keyframes slideUp {
260
+ from { opacity: 0; transform: translateY(50px); }
261
+ to { opacity: 1; transform: translateY(0); }
262
+ }
263
+
264
+ @keyframes fadeIn {
265
+ from { opacity: 0; }
266
+ to { opacity: 1; }
267
+ }
268
+
269
+ @keyframes spin {
270
+ 0% { transform: rotate(0deg); }
271
+ 100% { transform: rotate(360deg); }
272
+ }
273
+
274
+ @keyframes gradientShift {
275
+ 0%, 100% { background-position: 0% 50%; }
276
+ 50% { background-position: 100% 50%; }
277
+ }
278
+
279
+ @keyframes pulse {
280
+ 0%, 100% { opacity: 1; }
281
+ 50% { opacity: 0.7; }
282
+ }
283
+
284
+ /* Responsive design */
285
+ @media (max-width: 768px) {
286
+ .title {
287
+ font-size: 2.5rem;
288
+ }
289
+
290
+ .container {
291
+ padding: 1rem;
292
+ }
293
+
294
+ .upload-area {
295
+ min-height: 250px;
296
+ }
297
+ }
298
+ </style>
299
+ </head>
300
+ <body>
301
+ <div class="particles" id="particles"></div>
302
+
303
+ <div class="container">
304
+ <div class="header">
305
+ <h1 class="title">YOLO12 Vision AI</h1>
306
+ <p class="subtitle">Advanced Image Classification with Neural Networks</p>
307
+ </div>
308
+
309
+ <div class="upload-area" id="uploadArea">
310
+ <div class="upload-icon">🔮</div>
311
+ <div class="upload-text">Drop your image here or click to upload</div>
312
+ <div class="upload-subtext">Supports PNG, JPG, JPEG formats</div>
313
+ <input type="file" id="fileInput" class="file-input" accept=".png,.jpg,.jpeg">
314
+ </div>
315
+
316
+ <div class="preview-container" id="previewContainer" style="display: none;">
317
+ <img id="previewImage" class="preview-image" alt="Preview">
318
+ <button class="predict-button" id="predictButton">🚀 Analyze Image</button>
319
+ </div>
320
+
321
+ <div class="loading" id="loading">
322
+ <div class="spinner"></div>
323
+ <p style="color: #00d4ff; margin-top: 1rem;">Processing your image...</p>
324
+ </div>
325
+
326
+ <div class="result-container" id="resultContainer" style="display: none;">
327
+ <div class="result-class" id="resultClass"></div>
328
+ <div class="result-confidence">
329
+ Confidence: <span id="confidencePercentage"></span>
330
+ </div>
331
+ <div class="confidence-bar">
332
+ <div class="confidence-fill" id="confidenceFill"></div>
333
+ </div>
334
+ </div>
335
+
336
+ <div class="error" id="errorContainer" style="display: none;"></div>
337
+ </div>
338
+
339
+ <script>
340
+ // Create animated particles
341
+ function createParticles() {
342
+ const particlesContainer = document.getElementById('particles');
343
+ const particleCount = 50;
344
+
345
+ for (let i = 0; i < particleCount; i++) {
346
+ const particle = document.createElement('div');
347
+ particle.className = 'particle';
348
+ particle.style.left = Math.random() * 100 + '%';
349
+ particle.style.top = Math.random() * 100 + '%';
350
+ particle.style.animationDelay = Math.random() * 6 + 's';
351
+ particle.style.animationDuration = (3 + Math.random() * 3) + 's';
352
+ particlesContainer.appendChild(particle);
353
+ }
354
+ }
355
+
356
+ // Initialize particles
357
+ createParticles();
358
+
359
+ // DOM elements
360
+ const uploadArea = document.getElementById('uploadArea');
361
+ const fileInput = document.getElementById('fileInput');
362
+ const previewContainer = document.getElementById('previewContainer');
363
+ const previewImage = document.getElementById('previewImage');
364
+ const predictButton = document.getElementById('predictButton');
365
+ const loading = document.getElementById('loading');
366
+ const resultContainer = document.getElementById('resultContainer');
367
+ const errorContainer = document.getElementById('errorContainer');
368
+ const resultClass = document.getElementById('resultClass');
369
+ const confidencePercentage = document.getElementById('confidencePercentage');
370
+ const confidenceFill = document.getElementById('confidenceFill');
371
+
372
+ let selectedFile = null;
373
+
374
+ // Upload area click handler
375
+ uploadArea.addEventListener('click', () => {
376
+ fileInput.click();
377
+ });
378
+
379
+ // File input change handler
380
+ fileInput.addEventListener('change', handleFileSelect);
381
+
382
+ // Drag and drop handlers
383
+ uploadArea.addEventListener('dragover', (e) => {
384
+ e.preventDefault();
385
+ uploadArea.classList.add('dragover');
386
+ });
387
+
388
+ uploadArea.addEventListener('dragleave', () => {
389
+ uploadArea.classList.remove('dragover');
390
+ });
391
+
392
+ uploadArea.addEventListener('drop', (e) => {
393
+ e.preventDefault();
394
+ uploadArea.classList.remove('dragover');
395
+ const files = e.dataTransfer.files;
396
+ if (files.length > 0) {
397
+ handleFile(files[0]);
398
+ }
399
+ });
400
+
401
+ // Predict button handler
402
+ predictButton.addEventListener('click', predictImage);
403
+
404
+ function handleFileSelect(e) {
405
+ const file = e.target.files[0];
406
+ if (file) {
407
+ handleFile(file);
408
+ }
409
+ }
410
+
411
+ function handleFile(file) {
412
+ // Validate file type
413
+ const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg'];
414
+ if (!allowedTypes.includes(file.type)) {
415
+ showError('Please select a valid image file (PNG, JPG, JPEG)');
416
+ return;
417
+ }
418
+
419
+ // Validate file size (max 10MB)
420
+ if (file.size > 10 * 1024 * 1024) {
421
+ showError('File size too large. Please select a file smaller than 10MB.');
422
+ return;
423
+ }
424
+
425
+ selectedFile = file;
426
+
427
+ // Show preview
428
+ const reader = new FileReader();
429
+ reader.onload = (e) => {
430
+ previewImage.src = e.target.result;
431
+ previewContainer.style.display = 'block';
432
+ hideError();
433
+ hideResult();
434
+ };
435
+ reader.readAsDataURL(file);
436
+ }
437
+
438
+ async function predictImage() {
439
+ if (!selectedFile) {
440
+ showError('Please select an image first');
441
+ return;
442
+ }
443
+
444
+ // Show loading
445
+ loading.style.display = 'block';
446
+ predictButton.disabled = true;
447
+ hideError();
448
+ hideResult();
449
+
450
+ try {
451
+ const formData = new FormData();
452
+ formData.append('file', selectedFile);
453
+
454
+ const response = await fetch('/predict', {
455
+ method: 'POST',
456
+ body: formData
457
+ });
458
+
459
+ const data = await response.json();
460
+
461
+ if (response.ok) {
462
+ showResult(data);
463
+ } else {
464
+ showError(data.error || 'An error occurred during prediction');
465
+ }
466
+ } catch (error) {
467
+ showError('Failed to connect to the server. Please try again.');
468
+ console.error('Error:', error);
469
+ } finally {
470
+ loading.style.display = 'none';
471
+ predictButton.disabled = false;
472
+ }
473
+ }
474
+
475
+ function showResult(data) {
476
+ resultClass.textContent = data.class;
477
+ const confidence = Math.round(data.confidence * 100);
478
+ confidencePercentage.textContent = confidence + '%';
479
+ confidenceFill.style.width = confidence + '%';
480
+
481
+ resultContainer.style.display = 'block';
482
+
483
+ // Add some visual flair
484
+ setTimeout(() => {
485
+ confidenceFill.style.width = confidence + '%';
486
+ }, 100);
487
+ }
488
+
489
+ function showError(message) {
490
+ errorContainer.textContent = message;
491
+ errorContainer.style.display = 'block';
492
+ }
493
+
494
+ function hideError() {
495
+ errorContainer.style.display = 'none';
496
+ }
497
+
498
+ function hideResult() {
499
+ resultContainer.style.display = 'none';
500
+ }
501
+
502
+ // Add some interactive effects
503
+ document.addEventListener('mousemove', (e) => {
504
+ const particles = document.querySelectorAll('.particle');
505
+ const x = e.clientX / window.innerWidth;
506
+ const y = e.clientY / window.innerHeight;
507
+
508
+ particles.forEach((particle, index) => {
509
+ const speed = (index % 3 + 1) * 0.5;
510
+ const newX = x * speed;
511
+ const newY = y * speed;
512
+ particle.style.transform = `translate(${newX}px, ${newY}px)`;
513
+ });
514
+ });
515
+ </script>
516
+ </body>
517
+ </html>