ftx7go commited on
Commit
3bba4e3
·
verified ·
1 Parent(s): 79496c1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +453 -194
app.py CHANGED
@@ -1,203 +1,462 @@
1
- import os
2
- os.environ["CUDA_VISIBLE_DEVICES"] = "-1" # Force TensorFlow to use CPU
3
-
4
- import gradio as gr
5
  import numpy as np
6
- from PIL import Image
7
- from reportlab.lib.pagesizes import letter
8
- from reportlab.pdfgen import canvas
9
- from reportlab.lib import colors
10
- from reportlab.platypus import Table, TableStyle
11
- import requests
12
- import smtplib
13
- from email.mime.multipart import MIMEMultipart
14
- from email.mime.text import MIMEText
15
- from email.mime.base import MIMEBase
16
- from email import encoders
17
  import io
 
18
  import base64
19
-
20
- # FastAPI server URL
21
- FASTAPI_URL = "http://localhost:8000/analyze" # Updated to the default FastAPI port
22
-
23
- # Email credentials
24
- SENDER_EMAIL = "[email protected]"
25
- SENDER_PASSWORD = "1w3r5y7i9pW$"
26
-
27
- # Function to process X-ray and generate a PDF report
28
- def generate_report(name, age, gender, weight, height, allergies, cause, xray, email):
29
- image_size = (224, 224)
30
-
31
- # Send X-ray to FastAPI for analysis
32
- try:
33
- with open(xray, 'rb') as f:
34
- files = {'file': (os.path.basename(xray), f)}
35
- response = requests.post(FASTAPI_URL, files=files)
36
- response.raise_for_status() # Raise an exception for bad status codes
37
- fastapi_results_html = response.text
38
- except requests.exceptions.RequestException as e:
39
- return f"Error connecting to FastAPI server: {e}"
40
-
41
- # Extract prediction from FastAPI response (you might need to adjust this based on the exact HTML structure)
42
- diagnosed_class = "normal"
43
- severity = "Not Available"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  try:
45
- # Simple string matching for now - improve this if the HTML structure is complex
46
- if "KnochenWächter" in fastapi_results_html:
47
- if "Kein Knochenbruch" in fastapi_results_html:
48
- diagnosed_class = "normal"
49
- elif "Knochenbruch" in fastapi_results_html or "Auffällig" in fastapi_results_html:
50
- diagnosed_class = "Fractured"
51
-
52
- if diagnosed_class == "Fractured":
53
- if "score-high" in fastapi_results_html:
54
- severity = "Severe"
55
- elif "score-medium" in fastapi_results_html:
56
- severity = "Moderate"
57
- else:
58
- severity = "Mild"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  else:
60
- severity = "Mild" # Assuming normal is mild
61
- except Exception as e:
62
- print(f"Error parsing FastAPI response: {e}")
63
-
64
- # Treatment details table
65
- treatment_data = [
66
- ["Severity Level", "Recommended Treatment", "Recovery Duration"],
67
- ["Mild", "Rest, pain relievers, and follow-up X-ray", "4-6 weeks"],
68
- ["Moderate", "Plaster cast, minor surgery if needed", "6-10 weeks"],
69
- ["Severe", "Major surgery, metal implants, physiotherapy", "Several months"]
70
- ]
71
-
72
- # Estimated cost & duration table
73
- cost_duration_data = [
74
- ["Hospital Type", "Estimated Cost", "Recovery Time"],
75
- ["Government Hospital", f"₹{2000 if severity == 'Mild' else 8000 if severity == 'Moderate' else 20000} - ₹{5000 if severity == 'Mild' else 15000 if severity == 'Moderate' else 50000}", "4-12 weeks"],
76
- ["Private Hospital", f"₹{10000 if severity == 'Mild' else 30000 if severity == 'Moderate' else 100000}+", "6 weeks - Several months"]
77
- ]
78
-
79
- # Save X-ray image for report
80
- img = Image.open(xray).resize((300, 300))
81
- img_path = f"{name}_xray.png"
82
- img.save(img_path)
83
-
84
- # Generate PDF report
85
- report_path = f"{name}_fracture_report.pdf"
86
- c = canvas.Canvas(report_path, pagesize=letter)
87
-
88
- # Report title
89
- c.setFont("Helvetica-Bold", 16)
90
- c.drawString(200, 770, "Bone Fracture Detection Report")
91
-
92
- # Patient details table
93
- patient_data = [
94
- ["Patient Name", name],
95
- ["Age", age],
96
- ["Gender", gender],
97
- ["Weight", f"{weight} kg"],
98
- ["Height", f"{height} cm"],
99
- ["Allergies", allergies if allergies else "None"],
100
- ["Cause of Injury", cause if cause else "Not Provided"],
101
- ["Diagnosis", diagnosed_class],
102
- ["Injury Severity", severity]
103
- ]
104
-
105
- # Format and align tables
106
- def format_table(data):
107
- table = Table(data, colWidths=[270, 270]) # Set 90% width
108
- table.setStyle(TableStyle([
109
- ('BACKGROUND', (0, 0), (-1, 0), colors.darkblue),
110
- ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
111
- ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
112
- ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
113
- ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
114
- ('GRID', (0, 0), (-1, -1), 1, colors.black),
115
- ('VALIGN', (0, 0), (-1, -1), 'MIDDLE')
116
- ]))
117
- return table
118
-
119
- # Draw patient details table
120
- patient_table = format_table(patient_data)
121
- patient_table.wrapOn(c, 480, 500)
122
- patient_table.drawOn(c, 50, 620)
123
-
124
- # Load and insert X-ray image
125
- c.drawInlineImage(img_path, 50, 320, width=250, height=250)
126
- c.setFont("Helvetica-Bold", 12)
127
- c.drawString(120, 290, f"Fractured: {'Yes' if diagnosed_class == 'Fractured' else 'No'}")
128
-
129
- # Draw treatment and cost tables
130
- treatment_table = format_table(treatment_data)
131
- treatment_table.wrapOn(c, 480, 200)
132
- treatment_table.drawOn(c, 50, 200)
133
-
134
- cost_table = format_table(cost_duration_data)
135
- cost_table.wrapOn(c, 480, 150)
136
- cost_table.drawOn(c, 50, 80)
137
-
138
- c.save()
139
-
140
- # Send email with the report
141
- subject = "Bone Fracture Detection Report"
142
- body = f"Dear {name},\n\nPlease find attached your bone fracture detection report.\n\nSincerely,\nYour Bone Fracture Detection System"
143
-
144
- msg = MIMEMultipart()
145
- msg['From'] = SENDER_EMAIL
146
- msg['To'] = email
147
- msg['Subject'] = subject
148
- msg.attach(MIMEText(body))
149
-
150
- with open(report_path, "rb") as attachment:
151
- part = MIMEBase('application', "octet-stream")
152
- part.set_payload(attachment.read())
153
-
154
- encoders.encode_base64(part)
155
- part.add_header('Content-Disposition', f"attachment; filename= {os.path.basename(report_path)}")
156
- msg.attach(part)
 
 
157
 
158
- try:
159
- with smtplib.SMTP_SSL('smtp.gmail.com', 465) as server:
160
- server.login(SENDER_EMAIL, SENDER_PASSWORD)
161
- server.sendmail(SENDER_EMAIL, email, msg.as_string())
162
- print(f"Report sent successfully to {email}")
163
- return report_path # Return path for auto-download
164
  except Exception as e:
165
- return f"Error sending email: {e}"
166
-
167
- # Define Gradio Interface
168
- with gr.Blocks() as app:
169
- # Remove the HTML content loading
170
- gr.Markdown("## Bone Fracture Detection System")
171
-
172
- with gr.Row():
173
- name = gr.Textbox(label="Patient Name")
174
- age = gr.Number(label="Age")
175
- gender = gr.Radio(["Male", "Female", "Other"], label="Gender")
176
-
177
- with gr.Row():
178
- weight = gr.Number(label="Weight (kg)")
179
- height = gr.Number(label="Height (cm)")
180
-
181
- with gr.Row():
182
- allergies = gr.Textbox(label="Allergies (if any)")
183
- cause = gr.Textbox(label="Cause of Injury")
184
-
185
- with gr.Row():
186
- xray = gr.Image(type="filepath", label="Upload X-ray Image")
187
-
188
- # Remove the sample image selector
189
- email = gr.Textbox(label="Patient Email Address")
190
- submit_button = gr.Button("Generate Report and Send Email")
191
- output_file = gr.File(label="Download Report")
192
-
193
- # Remove the sample image loading functionality
194
-
195
- submit_button.click(
196
- generate_report,
197
- inputs=[name, age, gender, weight, height, allergies, cause, xray, email],
198
- outputs=[output_file],
199
- )
200
 
201
- # Launch the Gradio app
202
  if __name__ == "__main__":
203
- app.launch()
 
1
+ from fastapi import FastAPI, File, UploadFile
2
+ from fastapi.responses import HTMLResponse
3
+ from transformers import pipeline
4
+ from PIL import Image, ImageDraw
5
  import numpy as np
 
 
 
 
 
 
 
 
 
 
 
6
  import io
7
+ import uvicorn
8
  import base64
9
+ import random
10
+
11
+ app = FastAPI()
12
+
13
+ # Loading the models
14
+ def load_models():
15
+ return {
16
+ "BoneEye": pipeline("object-detection", model="D3STRON/bone-fracture-detr"),
17
+ "BoneGuard": pipeline("image-classification", model="Heem2/bone-fracture-detection-using-xray"),
18
+ "XRayMaster": pipeline("image-classification",
19
+ model="nandodeomkar/autotrain-fracture-detection-using-google-vit-base-patch-16-54382127388")
20
+ }
21
+
22
+ models = load_models()
23
+
24
+ def translate_label(label):
25
+ translations = {
26
+ "fracture": "Fracture",
27
+ "no fracture": "No Fracture",
28
+ "normal": "Normal",
29
+ "abnormal": "Abnormal",
30
+ "F1": "Fracture",
31
+ "NF": "No Fracture"
32
+ }
33
+ return translations.get(label.lower(), label)
34
+
35
+ def create_heatmap_overlay(image, box, score):
36
+ overlay = Image.new('RGBA', image.size, (0, 0, 0, 0))
37
+ draw = ImageDraw.Draw(overlay)
38
+
39
+ x1, y1 = box['xmin'], box['ymin']
40
+ x2, y2 = box['xmax'], box['ymax']
41
+
42
+ if score > 0.8:
43
+ fill_color = (255, 0, 0, 100)
44
+ border_color = (255, 0, 0, 255)
45
+ elif score > 0.6:
46
+ fill_color = (255, 165, 0, 100)
47
+ border_color = (255, 165, 0, 255)
48
+ else:
49
+ fill_color = (255, 255, 0, 100)
50
+ border_color = (255, 255, 0, 255)
51
+
52
+ draw.rectangle([x1, y1, x2, y2], fill=fill_color)
53
+ draw.rectangle([x1, y1, x2, y2], outline=border_color, width=2)
54
+
55
+ return overlay
56
+
57
+ def draw_boxes(image, predictions):
58
+ result_image = image.copy().convert('RGBA')
59
+
60
+ for pred in predictions:
61
+ box = pred['box']
62
+ score = pred['score']
63
+
64
+ overlay = create_heatmap_overlay(image, box, score)
65
+ result_image = Image.alpha_composite(result_image, overlay)
66
+
67
+ draw = ImageDraw.Draw(result_image)
68
+ temp = 36.5 + (score * 2.5)
69
+ label = f"{translate_label(pred['label'])} ({score:.1%} • {temp:.1f}°C)"
70
+
71
+ text_bbox = draw.textbbox((box['xmin'], box['ymin']-20), label)
72
+ draw.rectangle(text_bbox, fill=(0, 0, 0, 180))
73
+
74
+ draw.text(
75
+ (box['xmin'], box['ymin']-20),
76
+ label,
77
+ fill=(255, 255, 255, 255)
78
+ )
79
+
80
+ return result_image
81
+
82
+ def image_to_base64(image):
83
+ buffered = io.BytesIO()
84
+ image.save(buffered, format="PNG")
85
+ img_str = base64.b64encode(buffered.getvalue()).decode()
86
+ return f"data:image/png;base64,{img_str}"
87
+
88
+ COMMON_STYLES = """
89
+ body {
90
+ font-family: system-ui, -apple-system, sans-serif;
91
+ background: #f0f2f5;
92
+ margin: 0;
93
+ padding: 20px;
94
+ color: #1a1a1a;
95
+ }
96
+ ::-webkit-scrollbar {
97
+ width: 8px;
98
+ height: 8px;
99
+ }
100
+
101
+ ::-webkit-scrollbar-track {
102
+ background: transparent;
103
+ }
104
+
105
+ ::-webkit-scrollbar-thumb {
106
+ background-color: rgba(156, 163, 175, 0.5);
107
+ border-radius: 4px;
108
+ }
109
+
110
+ .container {
111
+ max-width: 1200px;
112
+ margin: 0 auto;
113
+ background: white;
114
+ padding: 20px;
115
+ border-radius: 10px;
116
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
117
+ }
118
+ .button {
119
+ background: #2d2d2d;
120
+ color: white;
121
+ border: none;
122
+ padding: 12px 30px;
123
+ border-radius: 8px;
124
+ cursor: pointer;
125
+ font-size: 1.1em;
126
+ transition: all 0.3s ease;
127
+ position: relative;
128
+ }
129
+ .button:hover {
130
+ background: #404040;
131
+ }
132
+ @keyframes progress {
133
+ 0% { width: 0; }
134
+ 100% { width: 100%; }
135
+ }
136
+ .button-progress {
137
+ position: absolute;
138
+ bottom: 0;
139
+ left: 0;
140
+ height: 4px;
141
+ background: rgba(255, 255, 255, 0.5);
142
+ width: 0;
143
+ }
144
+ .button:active .button-progress {
145
+ animation: progress 2s linear forwards;
146
+ }
147
+ img {
148
+ max-width: 100%;
149
+ height: auto;
150
+ border-radius: 8px;
151
+ }
152
+ @keyframes blink {
153
+ 0% { opacity: 1; }
154
+ 50% { opacity: 0; }
155
+ 100% { opacity: 1; }
156
+ }
157
+ #loading {
158
+ display: none;
159
+ color: white;
160
+ margin-top: 10px;
161
+ animation: blink 1s infinite;
162
+ text-align: center;
163
+ }
164
+ """
165
+
166
+ SAMPLE_IMAGES = [
167
+ {"id": "sample1", "filename": "sample1.png", "label": "Fracture"},
168
+ {"id": "sample2", "filename": "sample2.png", "label": "No Fracture"},
169
+ {"id": "sample3", "filename": "sample3.png", "label": "Fracture"},
170
+ {"id": "sample4", "filename": "sample4.png", "label": "No Fracture"},
171
+ {"id": "sample5", "filename": "sample5.png", "label": "Fracture"},
172
+ {"id": "sample6", "filename": "sample6.png", "label": "No Fracture"},
173
+ {"id": "sample7", "filename": "sample7.png", "label": "Fracture"},
174
+ {"id": "sample8", "filename": "sample8.png", "label": "No Fracture"},
175
+ {"id": "sample9", "filename": "sample9.png", "label": "Fracture"},
176
+ {"id": "sample10", "filename": "sample10.png", "label": "No Fracture"},
177
+ ]
178
+
179
+ @app.get("/", response_class=HTMLResponse)
180
+ async def main():
181
+ image_options = "".join(
182
+ f'<option value="{img["filename"]}">{img["id"]}</option>' for img in SAMPLE_IMAGES
183
+ )
184
+ content = f"""
185
+ <!DOCTYPE html>
186
+ <html>
187
+ <head>
188
+ <title>Fracture Detection</title>
189
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
190
+ <style>
191
+ {COMMON_STYLES}
192
+
193
+ .upload-section {{
194
+ background: #2d2d2d;
195
+ padding: 40px;
196
+ border-radius: 12px;
197
+ margin: 20px 0;
198
+ text-align: center;
199
+ border: 2px dashed #404040;
200
+ transition: all 0.3s ease;
201
+ color: white;
202
+ }}
203
+ .upload-section:hover {{
204
+ border-color: #555;
205
+ }}
206
+ .image-selection {{
207
+ font-size: 1.1em;
208
+ margin: 20px 0;
209
+ color: white;
210
+ }}
211
+ select {{
212
+ padding: 10px;
213
+ border-radius: 8px;
214
+ border: 1px solid #404040;
215
+ background: #2d2d2d;
216
+ color: white;
217
+ transition: all 0.3s ease;
218
+ cursor: pointer;
219
+ font-size: 1em;
220
+ }}
221
+ select:hover {{
222
+ background: #404040;
223
+ }}
224
+ .confidence-slider {{
225
+ width: 100%;
226
+ max-width: 300px;
227
+ margin: 20px auto;
228
+ }}
229
+ input[type="range"] {{
230
+ width: 100%;
231
+ height: 8px;
232
+ border-radius: 4px;
233
+ background: #404040;
234
+ outline: none;
235
+ transition: all 0.3s ease;
236
+ -webkit-appearance: none;
237
+ }}
238
+ input[type="range"]::-webkit-slider-thumb {{
239
+ -webkit-appearance: none;
240
+ width: 20px;
241
+ height: 20px;
242
+ border-radius: 50%;
243
+ background: white;
244
+ cursor: pointer;
245
+ border: none;
246
+ }}
247
+ </style>
248
+ </head>
249
+ <body>
250
+ <div class="container">
251
+ <div class="upload-section">
252
+ <form action="/analyze" method="post" enctype="multipart/form-data" onsubmit="document.getElementById('loading').style.display = 'block';">
253
+ <div class="image-selection">
254
+ <label for="sample_image">Select a Sample X-ray:</label>
255
+ <select name="sample_image" id="sample_image">
256
+ {image_options}
257
+ </select>
258
+ </div>
259
+ <div class="confidence-slider">
260
+ <label for="threshold">Confidence Threshold: <span id="thresholdValue">0.60</span></label>
261
+ <input type="range" id="threshold" name="threshold"
262
+ min="0" max="1" step="0.05" value="0.60"
263
+ oninput="document.getElementById('thresholdValue').textContent = parseFloat(this.value).toFixed(2)">
264
+ </div>
265
+ <button type="submit" class="button">
266
+ Analyze
267
+ <div class="button-progress"></div>
268
+ </button>
269
+ <div id="loading">Loading...</div>
270
+ </form>
271
+ </div>
272
+ </div>
273
+ </body>
274
+ </html>
275
+ """
276
+ return content
277
+
278
+ @app.post("/analyze", response_class=HTMLResponse)
279
+ async def analyze_file(sample_image: str, threshold: float = 0.60):
280
  try:
281
+ # For now, let's just pick a random sample image for demonstration.
282
+ # Replace this with actual loading of the selected image.
283
+ # You will need to place the sample images in a directory (e.g., 'sample_images')
284
+ image_path = f"sample_images/{sample_image}"
285
+ try:
286
+ image = Image.open(image_path)
287
+ except FileNotFoundError:
288
+ return f"""
289
+ <!DOCTYPE html>
290
+ <html>
291
+ <head>
292
+ <title>Error</title>
293
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
294
+ <style>
295
+ {COMMON_STYLES}
296
+ .error-box {{
297
+ background: #fee2e2;
298
+ border: 1px solid #ef4444;
299
+ padding: 20px;
300
+ border-radius: 8px;
301
+ margin: 20px 0;
302
+ }}
303
+ </style>
304
+ </head>
305
+ <body>
306
+ <div class="container">
307
+ <div class="error-box">
308
+ <h3>Error</h3>
309
+ <p>Sample image '{sample_image}' not found. Please ensure the image exists in the 'sample_images' directory.</p>
310
+ </div>
311
+ <a href="/" class="button back-button">
312
+ ← Back
313
+ <div class="button-progress"></div>
314
+ </a>
315
+ </div>
316
+ </body>
317
+ </html>
318
+ """
319
+
320
+ predictions_watcher = models["BoneGuard"](image)
321
+ predictions_master = models["XRayMaster"](image)
322
+ predictions_locator = models["BoneEye"](image)
323
+
324
+ filtered_preds = [p for p in predictions_locator if p['score'] >= threshold]
325
+ if filtered_preds:
326
+ result_image = draw_boxes(image, filtered_preds)
327
  else:
328
+ result_image = image
329
+
330
+ # Logic to make fractured area black will be implemented here once images and fracture data are available.
331
+ # For demonstration purposes, let's just mark the detected areas.
332
+
333
+ result_image_b64 = image_to_base64(result_image)
334
+
335
+ results_html = f"""
336
+ <!DOCTYPE html>
337
+ <html>
338
+ <head>
339
+ <title>Results</title>
340
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
341
+ <style>
342
+ {COMMON_STYLES}
343
+
344
+ .results-grid {{
345
+ display: grid;
346
+ grid-template-columns: 1fr 1fr;
347
+ gap: 20px;
348
+ margin-top: 20px;
349
+ }}
350
+ .result-box {{
351
+ background: white;
352
+ padding: 20px;
353
+ border-radius: 12px;
354
+ margin: 10px 0;
355
+ border: 1px solid #e9ecef;
356
+ }}
357
+ .score-high {{
358
+ color: #0066cc;
359
+ font-weight: bold;
360
+ }}
361
+
362
+ .score-medium {{
363
+ color: #ffa500;
364
+ font-weight: bold;
365
+ }}
366
+ .back-button {{
367
+ display: inline-block;
368
+ text-decoration: none;
369
+ margin-top: 20px;
370
+ }}
371
+ h3 {{
372
+ color: #0066cc;
373
+ margin-top: 0;
374
+ }}
375
+ @media (max-width: 768px) {{
376
+ .results-grid {{
377
+ grid-template-columns: 1fr;
378
+ }}
379
+ }}
380
+ </style>
381
+ </head>
382
+ <body>
383
+ <div class="container">
384
+ <div class="results-grid">
385
+ <div>
386
+ <div class="result-box"><h3>BoneGuard</h3>
387
+ """
388
+
389
+ for pred in predictions_watcher:
390
+ confidence_class = "score-high" if pred['score'] > 0.7 else "score-medium"
391
+ results_html += f"""
392
+ <div>
393
+ <span class="{confidence_class}">{pred['score']:.1%}</span> -
394
+ {translate_label(pred['label'])}
395
+ </div>
396
+ """
397
+ results_html += "</div>"
398
+
399
+ results_html += "<div class='result-box'><h3>XRayMaster</h3>"
400
+ for pred in predictions_master:
401
+ confidence_class = "score-high" if pred['score'] > 0.7 else "score-medium"
402
+ results_html += f"""
403
+ <div>
404
+ <span class="{confidence_class}">{pred['score']:.1%}</span> -
405
+ {translate_label(pred['label'])}
406
+ </div>
407
+ """
408
+ results_html += "</div></div>"
409
+
410
+ results_html += f"""
411
+ <div class='result-box'>
412
+ <h3>Fracture Localization</h3>
413
+ <img src="{result_image_b64}" alt="Analyzed image">
414
+ </div>
415
+ </div>
416
+
417
+ <a href="/" class="button back-button">
418
+ Back
419
+ <div class="button-progress"></div>
420
+ </a>
421
+ </div>
422
+ </body>
423
+ </html>
424
+ """
425
+
426
+ return results_html
427
 
 
 
 
 
 
 
428
  except Exception as e:
429
+ return f"""
430
+ <!DOCTYPE html>
431
+ <html>
432
+ <head>
433
+ <title>Error</title>
434
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
435
+ <style>
436
+ {COMMON_STYLES}
437
+ .error-box {{
438
+ background: #fee2e2;
439
+ border: 1px solid #ef4444;
440
+ padding: 20px;
441
+ border-radius: 8px;
442
+ margin: 20px 0;
443
+ }}
444
+ </style>
445
+ </head>
446
+ <body>
447
+ <div class="container">
448
+ <div class="error-box">
449
+ <h3>Error</h3>
450
+ <p>{str(e)}</p>
451
+ </div>
452
+ <a href="/" class="button back-button">
453
+ Back
454
+ <div class="button-progress"></div>
455
+ </a>
456
+ </div>
457
+ </body>
458
+ </html>
459
+ """
 
 
 
 
460
 
 
461
  if __name__ == "__main__":
462
+ uvicorn.run(app, host="0.0.0.0", port=7860)