Yash goyal commited on
Commit
70996b2
·
verified ·
1 Parent(s): 82107dd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +490 -192
app.py CHANGED
@@ -1,195 +1,493 @@
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>SnapSkin</title>
7
- <link rel="stylesheet" href="/static/form-styles.css" />
8
- <link rel="shortcut icon" href="/static/logo.png" />
9
- <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet"/>
10
- <script src="/static/preloader.js"></script>
11
- <script src="/static/cursor-effect.js"></script>
12
- <script defer>
13
- document.addEventListener("DOMContentLoaded", () => {
14
- const fileInput = document.getElementById("image");
15
- const fileMessage = document.getElementById("upload-message");
16
- const submitBtn = document.getElementById("submit-btn");
17
- const emailBtn = document.getElementById("email-report-btn");
18
-
19
- fileInput.addEventListener("change", () => {
20
- const file = fileInput.files[0];
21
- fileMessage.style.display = "block";
22
-
23
- if (!file) {
24
- fileMessage.textContent = "No file selected.";
25
- fileMessage.style.color = "orange";
26
- submitBtn.disabled = true;
27
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
- const allowedTypes = ["image/jpeg", "image/png"];
31
- const maxSize = 20 * 1024 * 1024; // 20MB
32
-
33
- if (!allowedTypes.includes(file.type)) {
34
- fileMessage.textContent = "Invalid file format. Use JPG or PNG.";
35
- fileMessage.style.color = "red";
36
- submitBtn.disabled = true;
37
- } else if (file.size > maxSize) {
38
- fileMessage.textContent = "File too large. Max 20MB allowed.";
39
- fileMessage.style.color = "red";
40
- submitBtn.disabled = true;
41
- } else {
42
- fileMessage.textContent = "✅ File ready for upload.";
43
- fileMessage.style.color = "limegreen";
44
- submitBtn.disabled = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  }
46
- });
47
-
48
- if (emailBtn) {
49
- emailBtn.addEventListener("click", async () => {
50
- const scanId = emailBtn.dataset.scanId;
51
- const emailStatus = document.getElementById("email-status");
52
- try {
53
- const response = await fetch(`/api/email-report/${scanId}`);
54
- const data = await response.json();
55
- emailStatus.textContent = data.message;
56
- emailStatus.style.color = data.success ? "limegreen" : "red";
57
- } catch (error) {
58
- emailStatus.textContent = "Error sending email.";
59
- emailStatus.style.color = "red";
60
- }
61
- });
62
- }
63
- });
64
- </script>
65
- </head>
66
- <body>
67
- <div class="preloader">
68
- <div class="preloader-particles"></div>
69
- <div class="dna-loader">
70
- <div class="dna-loader-strand"></div>
71
- <div class="dna-loader-strand"></div>
72
- <div class="dna-loader-rung"></div>
73
- <div class="dna-loader-rung"></div>
74
- <div class="dna-loader-rung"></div>
75
- <div class="dna-loader-rung"></div>
76
- <div class="dna-loader-rung"></div>
77
- <div class="dna-loader-rung"></div>
78
- <div class="dna-loader-rung"></div>
79
- <div class="dna-loader-rung"></div>
80
- <div class="loader-text">LOADING...</div>
81
- </div>
82
- </div>
83
-
84
- <header>
85
- <nav>
86
- <div class="logo" style="color: beige;">SNAP<span style="color: #00ffff;">SKIN</span></div>
87
- <ul>
88
- <li><a href="https://snapskin.vercel.app/">Home</a></li>
89
- </ul>
90
- </nav>
91
- </header>
92
-
93
- <section class="form-section">
94
- <div class="container">
95
- <div class="form-container">
96
- <h1 style="color: white;">ENTER PATIENT DETAILS</h1>
97
- <p class="intro-text">
98
- Fill out the form below for the diagnosis of your skin disease.
99
- Our AI will process your image and generate a predicted report for you.
100
- </p>
101
-
102
- <div class="form-wrapper">
103
- <form id="patient-form" action="/predict" method="POST" enctype="multipart/form-data">
104
- <div class="form-group">
105
- <label for="name">Full Name</label>
106
- <input type="text" id="name" name="name" placeholder="Enter your name" required />
107
- </div>
108
-
109
- <div class="form-group">
110
- <label for="email">Email Address</label>
111
- <input type="email" id="email" name="email" placeholder="Enter your email address" required />
112
- </div>
113
-
114
- <div class="form-row">
115
- <div class="form-group half">
116
- <label for="gender">Gender</label>
117
- <select id="gender" name="gender" required>
118
- <option value="" disabled selected>Select gender</option>
119
- <option value="male">Male</option>
120
- <option value="female">Female</option>
121
- <option value="other">Other</option>
122
- </select>
123
- </div>
124
-
125
- <div class="form-group half">
126
- <label for="age">Age</label>
127
- <input type="number" id="age" name="age" placeholder="Enter your age" min="1" max="120" required />
128
- </div>
129
- </div>
130
-
131
- <div class="form-group file-upload-group">
132
- <label for="image">Upload Image</label>
133
- <div class="file-upload-wrapper">
134
- <input type="file" id="image" name="image" accept=".jpg,.jpeg,.png" required />
135
- <span class="file-upload-text">Choose Image</span>
136
- </div>
137
- <small class="warning">Allowed: JPG, PNG | Max size: 20MB</small>
138
- <div class="upload-message" id="upload-message" style="display: none;"></div>
139
- </div>
140
-
141
- <div class="form-group consent-group">
142
- <input type="checkbox" id="consent" name="consent" required />
143
- <label for="consent">I agree to the <a href="#">Terms</a> and <a href="#">Privacy Policy</a></label>
144
- </div>
145
-
146
- <div class="form-actions">
147
- <button type="submit" class="submit-button" id="submit-btn">Submit Details</button>
148
- </div>
149
- </form>
150
-
151
- <!-- Result Section -->
152
- <div class="result-section" id="result-section" style="margin-top: 30px; {% if result %}display: block;{% else %}display: none;{% endif %}">
153
- <h2>Diagnosis Result</h2>
154
- <div id="result-content">
155
- {% if result %}
156
- <p><strong>Prediction:</strong> {{ result.prediction }}</p>
157
- <p><strong>Confidence:</strong> {{ result.confidence }}</p>
158
- {% if result.message %}
159
- <p class="warning-message">{{ result.message }}</p>
160
- {% endif %}
161
- <p id="email-status" style="color: {% if 'Failed' in result.email_status %}red{% else %}limegreen{% endif %}">{{ result.email_status }}</p>
162
- <button id="email-report-btn" class="submit-button" data-scan-id="{{ result.scan_id }}">Resend Report to Email</button>
163
- {% endif %}
164
- </div>
165
- </div>
166
- </div>
167
- </div>
168
- </div>
169
- </section>
170
-
171
- <footer>
172
- <div class="container">
173
- <div class="footer-content">
174
- <div class="footer-logo">SNAP<span>SKIN</span></div>
175
- <div class="footer-links">
176
- <h3>Quick Links</h3>
177
- <ul>
178
- <li><a href="/form">Home</a></li>
179
- <li><a href="#">About</a></li>
180
- <li><a href="#">Features</a></li>
181
- </ul>
182
- </div>
183
- <div class="footer-contact">
184
- <h3>Support</h3>
185
- <p>Email: info.snapskin@gmail.com</p>
186
- <p>Phone: 7817833974 / 7983595318</p>
187
- </div>
188
- </div>
189
- <div class="copyright">
190
- <p>© 2025 SnapSkin. All rights reserved.</p>
191
- </div>
192
- </div>
193
- </footer>
194
- </body>
195
- </html>
 
1
+ import os
2
+ os.environ['MPLCONFIGDIR'] = '/tmp/matplotlib'
3
+ from flask import Flask, render_template, request, redirect, url_for, session, send_file
4
+ from flask_sqlalchemy import SQLAlchemy
5
+ from flask_migrate import Migrate
6
+ import tensorflow as tf
7
+ import numpy as np
8
+ from PIL import Image
9
+ import pickle
10
+ import io
11
+ import os
12
+ import matplotlib.pyplot as plt
13
+ from reportlab.lib.pagesizes import A4
14
+ from reportlab.lib import colors
15
+ from reportlab.pdfgen import canvas
16
+ from reportlab.lib.units import inch
17
+ from datetime import datetime
18
+ import logging
19
+ from flask_mail import Mail, Message
20
+ from flask import jsonify, url_for
21
+
22
+ app = Flask(__name__)
23
+ app.secret_key = "e3f6f40bb8b2471b9f07c4025d845be9"
24
+
25
+ # Database configuration
26
+ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/snapsin.db'
27
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
28
+ db = SQLAlchemy(app)
29
+ migrate = Migrate(app, db)
30
+
31
+ # Mail configuration
32
+ app.config['MAIL_SERVER'] = 'smtp.gmail.com'
33
+ app.config['MAIL_PORT'] = 465
34
+ app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
35
+ app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
36
+ app.config['MAIL_USE_TLS'] = False
37
+ app.config['MAIL_USE_SSL'] = True
38
+ mail = Mail(app)
39
+
40
+ MODEL_PATH = "skin_lesion_model.h5"
41
+ HISTORY_PATH = "training_history.pkl"
42
+ PLOT_PATH = "/tmp/static/training_plot.png"
43
+ LOGO_PATH = "static/logo.jpg"
44
+ IMG_SIZE = (224, 224)
45
+ CONFIDENCE_THRESHOLD = 0.30
46
+
47
+ label_map = {
48
+ 0: "Melanoma",
49
+ 1: "Melanocytic nevus",
50
+ 2: "Basal cell carcinoma",
51
+ 3: "Actinic keratosis",
52
+ 4: "Benign keratosis",
53
+ 5: "Dermatofibroma",
54
+ 6: "Vascular lesion",
55
+ 7: "Squamous cell carcinoma"
56
+ }
57
+
58
+ recommendations = {
59
+ "Melanoma": {
60
+ "solutions": [
61
+ "Consult a dermatologist immediately.",
62
+ "Surgical removal is typically required.",
63
+ "Regular follow-up and screening for metastasis."
64
+ ],
65
+ "medications": ["Interferon alfa-2b", "Vemurafenib", "Dacarbazine"]
66
+ },
67
+ "Melanocytic nevus": {
68
+ "solutions": [
69
+ "Usually benign and requires no treatment.",
70
+ "Monitor for any change in shape or color."
71
+ ],
72
+ "medications": ["No medication necessary unless changes occur."]
73
+ },
74
+ "Basal cell carcinoma": {
75
+ "solutions": [
76
+ "Surgical excision or Mohs surgery.",
77
+ "Topical treatments if superficial.",
78
+ "Radiation in select cases."
79
+ ],
80
+ "medications": ["Imiquimod cream", "Fluorouracil cream", "Vismodegib"]
81
+ },
82
+ "Actinic keratosis": {
83
+ "solutions": [
84
+ "Cryotherapy or topical treatments.",
85
+ "Avoid prolonged sun exposure.",
86
+ "Use of sunscreen regularly."
87
+ ],
88
+ "medications": ["Fluorouracil", "Imiquimod", "Diclofenac gel"]
89
+ },
90
+ "Benign keratosis": {
91
+ "solutions": [
92
+ "Generally harmless and often left untreated.",
93
+ "Can be removed for cosmetic reasons."
94
+ ],
95
+ "medications": ["No medication required unless infected."]
96
+ },
97
+ "Dermatofibroma": {
98
+ "solutions": [
99
+ "Benign skin growth, no treatment needed.",
100
+ "Surgical removal if painful or for cosmetic reasons."
101
+ ],
102
+ "medications": ["No medication needed."]
103
+ },
104
+ "Vascular lesion": {
105
+ "solutions": [
106
+ "Treatment depends on type (e.g., hemangioma).",
107
+ "Laser therapy is commonly used.",
108
+ "Observation if no complications."
109
+ ],
110
+ "medications": ["Beta-blockers (e.g., propranolol for hemangioma)"]
111
+ },
112
+ "Squamous cell carcinoma": {
113
+ "solutions": [
114
+ "Surgical removal is standard.",
115
+ "Follow-up for recurrence or metastasis.",
116
+ "Avoid sun exposure and use sunscreen."
117
+ ],
118
+ "medications": ["Fluorouracil", "Cisplatin", "Imiquimod"]
119
+ },
120
+ "Low confidence": {
121
+ "solutions": [
122
+ "The image is not confidently classified.",
123
+ "Please upload a clearer image or consult a doctor."
124
+ ],
125
+ "medications": ["Not available due to low confidence."]
126
+ },
127
+ "Unknown": {
128
+ "solutions": ["No specific guidance available."],
129
+ "medications": ["N/A"]
130
+ }
131
+ }
132
+
133
+ # Logger
134
+ logging.basicConfig(level=logging.INFO)
135
+ logger = logging.getLogger(__name__)
136
+
137
+ # Database Models
138
+ class User(db.Model):
139
+ id = db.Column(db.Integer, primary_key=True)
140
+ name = db.Column(db.String(100), nullable=False)
141
+ email = db.Column(db.String(120), unique=True, nullable=False)
142
+ scans = db.relationship('Scan', backref='user', lazy=True)
143
+
144
+ class Scan(db.Model):
145
+ id = db.Column(db.Integer, primary_key=True)
146
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
147
+ patient_name = db.Column(db.String(100), nullable=False)
148
+ patient_gender = db.Column(db.String(20), nullable=False)
149
+ patient_age = db.Column(db.Integer, nullable=False)
150
+ prediction = db.Column(db.String(100), nullable=False)
151
+ confidence = db.Column(db.String(20), nullable=False)
152
+ timestamp = db.Column(db.DateTime, default=datetime.utcnow)
153
+ image_filename = db.Column(db.String(100), nullable=False)
154
+
155
+ # Load Model
156
+ try:
157
+ logger.info("Loading model from %s", MODEL_PATH)
158
+ model = tf.keras.models.load_model(MODEL_PATH)
159
+ except Exception as e:
160
+ logger.error("Failed to load model: %s", str(e))
161
+ raise
162
+
163
+ # Plot training history
164
+ if os.path.exists(HISTORY_PATH):
165
+ try:
166
+ with open(HISTORY_PATH, "rb") as f:
167
+ history_dict = pickle.load(f)
168
+ if "accuracy" in history_dict and "val_accuracy" in history_dict:
169
+ os.makedirs("/tmp/static", exist_ok=True)
170
+ plt.plot(history_dict['accuracy'], label='Train Accuracy')
171
+ plt.plot(history_dict['val_accuracy'], label='Val Accuracy')
172
+ plt.xlabel('Epochs')
173
+ plt.ylabel('Accuracy')
174
+ plt.title('Training History')
175
+ plt.legend()
176
+ plt.grid(True)
177
+ plt.savefig(PLOT_PATH)
178
+ plt.close()
179
+ logger.info("Training plot saved at %s", PLOT_PATH)
180
+ except Exception as e:
181
+ logger.error("Training history load error: %s", str(e))
182
+
183
+ def preprocess_image(image_bytes):
184
+ image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
185
+ image = image.resize(IMG_SIZE)
186
+ image_array = tf.keras.utils.img_to_array(image)
187
+ return np.expand_dims(image_array, axis=0) / 255.0
188
+
189
+ def generate_pdf(report, filepath):
190
+ c = canvas.Canvas(filepath, pagesize=A4)
191
+ width, height = A4
192
+ y = height - 60
193
+
194
+ # Background
195
+ c.setFillColor(colors.Color(0.98, 0.98, 0.99, alpha=1))
196
+ c.rect(0, 0, width, height, fill=1, stroke=0)
197
+
198
+ # Header background
199
+ c.setFillColor(colors.Color(0.94, 0.96, 0.98, alpha=1))
200
+ c.rect(0, height-120, width, 120, fill=1, stroke=0)
201
+
202
+ # Logo from root directory - square JPG format
203
+ try:
204
+ logo_path = "./logo.jpg"
205
+ if os.path.exists(logo_path):
206
+ c.setFillColor(colors.white)
207
+ c.rect(65, y-25, 50, 50, fill=1, stroke=1)
208
+ c.setStrokeColor(colors.Color(0.7, 0.7, 0.7, alpha=1))
209
+ c.setLineWidth(1)
210
+ c.rect(65, y-25, 50, 50, fill=0, stroke=1)
211
+ c.drawImage(logo_path, 67, y-23, width=46, height=46, preserveAspectRatio=True, mask='auto')
212
+ except Exception as e:
213
+ logger.warning("Logo error: %s", str(e))
214
+
215
+ # Professional title
216
+ c.setFont("Helvetica-Bold", 22)
217
+ c.setFillColor(colors.Color(0.2, 0.2, 0.2, alpha=1))
218
+ c.drawCentredString(width / 2, y + 5, "Medical Diagnosis Report")
219
+
220
+ # Subtitle
221
+ c.setFont("Helvetica", 11)
222
+ c.setFillColor(colors.Color(0.5, 0.5, 0.5, alpha=1))
223
+ c.drawCentredString(width / 2, y - 15, "Dermatological Analysis")
224
+
225
+ # Professional line
226
+ c.setStrokeColor(colors.Color(0.8, 0.8, 0.8, alpha=1))
227
+ c.setLineWidth(1)
228
+ c.line(80, y - 35, width - 80, y - 35)
229
+
230
+ y -= 80
231
+
232
+ def professional_section_box(title, fields, extra_gap=20):
233
+ nonlocal y
234
+
235
+ box_height = len(fields) * 20 + 40
236
+ c.setFillColor(colors.Color(0.96, 0.96, 0.96, alpha=0.3))
237
+ c.rect(42, y - box_height - 2, width - 84, box_height, fill=1, stroke=0)
238
+
239
+ c.setFillColor(colors.white)
240
+ c.rect(40, y - box_height, width - 80, box_height, fill=1, stroke=1)
241
+ c.setStrokeColor(colors.Color(0.9, 0.9, 0.9, alpha=1))
242
+
243
+ c.setFillColor(colors.Color(0.95, 0.95, 0.95, alpha=1))
244
+ c.rect(40, y - 30, width - 80, 30, fill=1, stroke=0)
245
+
246
+ c.setFont("Helvetica-Bold", 12)
247
+ c.setFillColor(colors.Color(0.3, 0.3, 0.3, alpha=1))
248
+ c.drawString(55, y - 20, title)
249
+
250
+ y -= 45
251
+ c.setFont("Helvetica", 10)
252
+ c.setFillColor(colors.Color(0.2, 0.2, 0.2, alpha=1))
253
+
254
+ for label, val in fields.items():
255
+ c.setFont("Helvetica-Bold", 9)
256
+ c.setFillColor(colors.Color(0.4, 0.4, 0.4, alpha=1))
257
+ c.drawString(55, y, f"{label}:")
258
+ c.setFont("Helvetica", 9)
259
+ c.setFillColor(colors.Color(0.2, 0.2, 0.2, alpha=1))
260
+ c.drawString(150, y, str(val))
261
+ y -= 20
262
+
263
+ y -= extra_gap
264
+
265
+ professional_section_box("Patient Information", {
266
+ "Name": report["name"],
267
+ "Email": report["email"],
268
+ "Gender": report["gender"],
269
+ "Age": f"{report['age']} years"
270
+ })
271
+
272
+ confidence_val = float(report["confidence"].replace('%', ''))
273
+ confidence_text = f"{report['confidence']} ({'High' if confidence_val > 85 else 'Moderate' if confidence_val > 70 else 'Low'} Confidence)"
274
+
275
+ professional_section_box("Diagnostic Results", {
276
+ "Condition": report["prediction"],
277
+ "Confidence": confidence_text,
278
+ "Notes": report["message"] if report["message"] else "No additional notes"
279
+ })
280
+
281
+ disease = report["prediction"]
282
+ treatment = recommendations.get(disease, recommendations["Unknown"])
283
+
284
+ professional_section_box("Treatment Recommendations", {
285
+ f"{i+1}. {line}": "" for i, line in enumerate(treatment["solutions"])
286
+ })
287
+
288
+ professional_section_box("Medication Guidelines", {
289
+ f"{i+1}. {line}": "" for i, line in enumerate(treatment["medications"])
290
+ })
291
+
292
+ c.setFillColor(colors.Color(0.98, 0.98, 0.98, alpha=1))
293
+ c.rect(40, 40, width - 80, 70, fill=1, stroke=1)
294
+ c.setStrokeColor(colors.Color(0.9, 0.9, 0.9, alpha=1))
295
+
296
+ c.setFont("Helvetica-Bold", 10)
297
+ c.setFillColor(colors.Color(0.4, 0.4, 0.4, alpha=1))
298
+ c.drawString(50, 95, "Medical Disclaimer")
299
+
300
+ c.setFont("Helvetica", 8)
301
+ c.setFillColor(colors.Color(0.3, 0.3, 0.3, alpha=1))
302
+ disclaimer_lines = [
303
+ "This report is generated using AI technology for preliminary assessment purposes only.",
304
+ "Results should not replace professional medical consultation and diagnosis.",
305
+ "Please consult a qualified healthcare provider for comprehensive medical evaluation."
306
+ ]
307
+
308
+ for i, line in enumerate(disclaimer_lines):
309
+ c.drawString(50, 80 - (i * 10), line)
310
+
311
+ c.save()
312
+
313
+ @app.route("/")
314
+ def home():
315
+ return redirect(url_for("form"))
316
+
317
+ @app.route("/form")
318
+ def form():
319
+ return render_template("form.html", history_plot="/training_plot.png")
320
+
321
+ @app.route("/training_plot.png")
322
+ def training_plot():
323
+ return send_file(PLOT_PATH, mimetype="image/png")
324
+
325
+ @app.route("/api/history")
326
+ def api_history():
327
+ user_email = request.args.get('email')
328
+ if not user_email:
329
+ return jsonify({"error": "Email parameter is required"}), 400
330
+
331
+ user = User.query.filter_by(email=user_email).first()
332
+ if not user:
333
+ return jsonify([])
334
+
335
+ scans = Scan.query.filter_by(user_id=user.id).order_by(Scan.timestamp.desc()).all()
336
+
337
+ history_data = []
338
+ for scan in scans:
339
+ image_url = url_for('uploaded_file', filename=scan.image_filename, _external=True)
340
+ history_data.append({
341
+ "id": scan.id,
342
+ "prediction": scan.prediction,
343
+ "confidence": scan.confidence,
344
+ "timestamp": scan.timestamp.strftime("%B %d, %Y at %I:%M %p"),
345
+ "patient_name": scan.patient_name,
346
+ "image_url": image_url
347
+ })
348
+
349
+ return jsonify(history_data)
350
+
351
+ @app.route("/api/email-report/<int:scan_id>")
352
+ def email_report(scan_id):
353
+ scan = Scan.query.get(scan_id)
354
+ if not scan:
355
+ return jsonify({"error": "Report not found"}), 404
356
+
357
+ try:
358
+ report_data = {
359
+ "name": scan.user.name,
360
+ "email": scan.user.email,
361
+ "gender": scan.patient_gender,
362
+ "age": scan.patient_age,
363
+ "prediction": scan.prediction,
364
+ "confidence": scan.confidence,
365
+ "message": ""
366
  }
367
+ pdf_path = f"/tmp/report_{scan_id}.pdf"
368
+ generate_pdf(report_data, pdf_path)
369
+
370
+ msg = Message(
371
+ 'Your SnapSkin Diagnostic Report',
372
+ sender=app.config['MAIL_USERNAME'],
373
+ recipients=[scan.user.email]
374
+ )
375
+ msg.body = f"Dear {scan.user.name},\n\nPlease find your requested diagnostic report attached.\n\nThank you for using SnapSkin."
376
+ with app.open_resource(pdf_path) as fp:
377
+ msg.attach(f"SnapSkin_Report_{scan_id}.pdf", "application/pdf", fp.read())
378
+
379
+ mail.send(msg)
380
+ os.remove(pdf_path)
381
+
382
+ return jsonify({"success": True, "message": f"Report sent to {scan.user.email}"})
383
 
384
+ except Exception as e:
385
+ logger.error(f"Failed to send email for scan {scan_id}: {e}")
386
+ return jsonify({"success": False, "message": "Failed to send email."}), 500
387
+
388
+ @app.route("/predict", methods=["POST"])
389
+ def predict():
390
+ try:
391
+ if "image" not in request.files:
392
+ raise ValueError("No image uploaded.")
393
+ image = request.files["image"]
394
+ image_bytes = image.read()
395
+ img_array = preprocess_image(image_bytes)
396
+ prediction = model.predict(img_array)[0]
397
+ predicted_index = int(np.argmax(prediction))
398
+ confidence = float(prediction[predicted_index])
399
+ label = label_map.get(predicted_index, "Unknown") if confidence >= CONFIDENCE_THRESHOLD else "Low confidence"
400
+ msg = "⚠ This image is not confidently recognized. Please upload a clearer image." if confidence < CONFIDENCE_THRESHOLD else ""
401
+
402
+ # Save user and scan data
403
+ email = request.form.get("email")
404
+ user = User.query.filter_by(email=email).first()
405
+ if not user:
406
+ user = User(name=request.form.get("name"), email=email)
407
+ db.session.add(user)
408
+ db.session.commit()
409
+
410
+ # Save image
411
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
412
+ image_filename = f"scan_{timestamp}.jpg"
413
+ image_path = os.path.join("static/uploads", image_filename)
414
+ os.makedirs("static/uploads", exist_ok=True)
415
+ image.seek(0)
416
+ image.save(image_path)
417
+
418
+ scan = Scan(
419
+ user_id=user.id,
420
+ patient_name=request.form.get("name"),
421
+ patient_gender=request.form.get("gender"),
422
+ patient_age=int(request.form.get("age")),
423
+ prediction=label,
424
+ confidence=f"{confidence * 100:.2f}%",
425
+ image_filename=image_filename
426
+ )
427
+ db.session.add(scan)
428
+ db.session.commit()
429
+
430
+ report = {
431
+ "name": request.form.get("name"),
432
+ "email": email,
433
+ "gender": request.form.get("gender"),
434
+ "age": request.form.get("age"),
435
+ "prediction": label,
436
+ "confidence": f"{confidence * 100:.2f}%",
437
+ "message": msg,
438
+ "scan_id": scan.id
439
  }
440
+ session["report"] = report
441
+
442
+ # Send email automatically
443
+ try:
444
+ pdf_path = f"/tmp/report_{scan.id}.pdf"
445
+ generate_pdf(report, pdf_path)
446
+ msg = Message(
447
+ 'Your SnapSkin Diagnostic Report',
448
+ sender=app.config['MAIL_USERNAME'],
449
+ recipients=[email]
450
+ )
451
+ msg.body = f"Dear {report['name']},\n\nPlease find your diagnostic report attached.\n\nThank you for using SnapSkin."
452
+ with app.open_resource(pdf_path) as fp:
453
+ msg.attach(f"SnapSkin_Report_{scan.id}.pdf", "application/pdf", fp.read())
454
+ mail.send(msg)
455
+ os.remove(pdf_path)
456
+ report["email_status"] = "Report sent to your email."
457
+ except Exception as e:
458
+ logger.error(f"Failed to send email: {e}")
459
+ report["email_status"] = "Failed to send report to email."
460
+
461
+ return redirect(url_for("result"))
462
+ except Exception as e:
463
+ return render_template("form.html", history_plot="/training_plot.png", result={
464
+ "prediction": "Error",
465
+ "confidence": "N/A",
466
+ "message": str(e),
467
+ "email_status": "Error occurred, no email sent."
468
+ })
469
+
470
+ @app.route("/result")
471
+ def result():
472
+ report = session.get("report", {})
473
+ return render_template("result.html", **report)
474
+
475
+ @app.route("/download-report")
476
+ def download_report():
477
+ report = session.get("report", {})
478
+ if not report:
479
+ return redirect(url_for("form"))
480
+ os.makedirs("/tmp/reports", exist_ok=True)
481
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
482
+ filepath = f"/tmp/reports/report_{timestamp}.pdf"
483
+ generate_pdf(report, filepath)
484
+ return send_file(filepath, as_attachment=True)
485
+
486
+ @app.route("/uploads/<filename>")
487
+ def uploaded_file(filename):
488
+ return send_file(os.path.join("static/uploads", filename))
489
+
490
+ if __name__ == "__main__":
491
+ with app.app_context():
492
+ db.create_all()
493
+ app.run(host="0.0.0.0", port=7860)