webapp1 commited on
Commit
ced9642
·
verified ·
1 Parent(s): 33f70bb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +73 -109
app.py CHANGED
@@ -17,15 +17,12 @@ import torch.nn as nn
17
  import numpy as np
18
  import pandas as pd
19
  import joblib
20
- import smtplib
21
- import ssl
22
  import threading
23
  import uuid
24
  import time
25
  import requests
26
  import datetime
27
  from urllib.parse import urlparse
28
- from email.message import EmailMessage
29
  from flask import Flask, request, render_template, jsonify, Response, send_from_directory
30
  from werkzeug.utils import secure_filename
31
 
@@ -56,11 +53,9 @@ print("--- BOOT SEQUENCE INITIATED ---", flush=True)
56
 
57
  app = Flask(__name__)
58
 
59
- ALERT_EMAIL_SENDER = "gowreeshgowri50@gmail.com"
60
- # FIX: Automatically strip spaces from Google App Password
61
- ALERT_EMAIL_PASSWORD = "oynu ulet pynk xsza".replace(" ", "")
62
- ALERT_EMAIL_RECEIVER = "mcblackdevil12342@gmail.com"
63
- ENABLE_EMAIL_ALERTS = True
64
 
65
  UPLOAD_FOLDER = 'uploads'
66
  MODEL_FOLDER = 'models'
@@ -99,7 +94,6 @@ except:
99
  # --- Helper Functions ---
100
 
101
  def get_geo_info(ip_or_url=None):
102
- """Fetches location and weather based on IP or URL."""
103
  try:
104
  target = ""
105
  if ip_or_url:
@@ -130,40 +124,19 @@ def process_accident_async(stream_id, frame, location, confidence, severity):
130
  if stream_id in active_streams:
131
  active_streams[stream_id]["plates"] = detected_plates
132
 
133
- # --- FOOLPROOF PUSH NOTIFICATION (Bypasses HuggingFace SMTP Blocks) ---
134
  try:
135
- ntfy_url = "https://ntfy.sh/crashvision_sos_alerts"
136
  headers = {
137
  "Title": f"🚨 {severity.upper()} ACCIDENT DETECTED",
138
- "Priority": "high",
139
- "Tags": "warning,rotating_light,car"
 
140
  }
141
- msg_body = f"Location: {location}\nSeverity: {severity.capitalize()}\nConfidence: {confidence}%\nTime: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
142
- requests.post(ntfy_url, data=msg_body.encode('utf-8'), headers=headers, timeout=5)
143
- print("📱 Push Notification SOS sent via ntfy.sh!", flush=True)
144
  except Exception as e:
145
- print(f"📱 Push Notification Failed: {e}", flush=True)
146
-
147
- # --- EMAIL FALLBACK ---
148
- if ENABLE_EMAIL_ALERTS:
149
- try:
150
- _, buffer = cv2.imencode('.jpg', frame)
151
- msg = EmailMessage()
152
- msg['Subject'] = f"🚨 ALERT: {severity.upper()} ACCIDENT DETECTED"
153
- msg['From'] = ALERT_EMAIL_SENDER
154
- msg['To'] = ALERT_EMAIL_RECEIVER
155
- msg.set_content(f"Incident Report\nLocation: {location}\nSeverity: {severity}\nConfidence: {confidence}%\nTime: {datetime.datetime.now()}\n\nSystem has locked this stream for investigation.")
156
-
157
- msg.add_attachment(buffer.tobytes(), maintype='image', subtype='jpeg', filename='incident.jpg')
158
-
159
- context = ssl.create_default_context()
160
- with smtplib.SMTP_SSL('smtp.gmail.com', 465, context=context, timeout=15) as server:
161
- server.login(ALERT_EMAIL_SENDER, ALERT_EMAIL_PASSWORD)
162
- server.send_message(msg)
163
-
164
- print(f"📧 Dispatch Email Sent Successfully for Stream {stream_id}", flush=True)
165
- except Exception as e:
166
- print(f"📧 Email Failed to Send (Likely Cloud Firewall): {str(e)}", flush=True)
167
 
168
  def run_temporal_analysis(stream_id, frames_3d_copy, prob_max, prob_mean, prob_min, yolo_max_conf, raw_frame, location):
169
  state = active_streams.get(stream_id)
@@ -183,7 +156,6 @@ def run_temporal_analysis(stream_id, frames_3d_copy, prob_max, prob_mean, prob_m
183
  confidence = float(ensemble_probs[final_idx] * 100)
184
  severity = classes[final_idx]
185
  except:
186
- # Fallback if SVM isn't perfectly aligned
187
  final_idx = min(int(np.argmax(prob_max)), 2)
188
  confidence = float(np.max(prob_max)) * 100
189
  severity = classes[final_idx]
@@ -193,7 +165,6 @@ def run_temporal_analysis(stream_id, frames_3d_copy, prob_max, prob_mean, prob_m
193
  state["cnn"] = round(yolo_max_conf, 1)
194
  state["rcnn"] = round(float(np.max(prob_3d)) * 100, 1)
195
 
196
- # Lock the decision only if confidence crosses threshold (Lowered to 75 to ensure triggers)
197
  if confidence > 75:
198
  state["final_decision"] = True
199
  state["label"] = f"Incident Logged: {severity.capitalize()}"
@@ -202,8 +173,7 @@ def run_temporal_analysis(stream_id, frames_3d_copy, prob_max, prob_mean, prob_m
202
  state["label"] = f"Analyzing... ({severity.capitalize()})"
203
 
204
  except Exception as e:
205
- print(f"AI Error in Analysis Thread: {e}", flush=True)
206
- traceback.print_exc()
207
  finally:
208
  if state: state["is_analyzing"] = False
209
 
@@ -229,16 +199,14 @@ def init_upload():
229
  def init_stream():
230
  url = request.json.get('url')
231
 
232
- # --- YouTube Live Stream Resolution ---
233
  if 'youtube.com' in url or 'youtu.be' in url:
234
  try:
235
  ydl_opts = {'format': 'best[ext=mp4]/best/bestvideo', 'quiet': True, 'noplaylist': True}
236
  with yt_dlp.YoutubeDL(ydl_opts) as ydl:
237
  info = ydl.extract_info(url, download=False)
238
- if 'url' in info:
239
- url = info['url']
240
- elif 'formats' in info and len(info['formats']) > 0:
241
- url = info['formats'][-1]['url']
242
  except Exception as e:
243
  print(f"yt-dlp extraction failed: {e}", flush=True)
244
 
@@ -255,14 +223,13 @@ def init_stream():
255
 
256
  @app.route('/video_control/<stream_id>', methods=['POST'])
257
  def video_control(stream_id):
258
- """Dynamic Video Controls for the AI Player"""
259
  state = active_streams.get(stream_id)
260
  if not state: return jsonify({"error": "not found"}), 404
261
 
262
  action = request.json.get('action')
263
  if action == 'toggle_tracking':
264
- state["show_tracking"] = request.json.get("track", True)
265
- state["force_update"] = True # Force frame redraw even if paused
266
  elif action == 'pause': state["paused"] = True
267
  elif action == 'play': state["paused"] = False
268
  elif action == 'seek': state["seek_to"] = request.json.get("value", 0.0)
@@ -273,7 +240,7 @@ def video_control(stream_id):
273
  def video_stream_gen(stream_id, source, location):
274
  cap = cv2.VideoCapture(source)
275
  fps = cap.get(cv2.CAP_PROP_FPS)
276
- if not fps or fps == 0: fps = 30.0
277
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
278
  is_live = str(source).startswith('http')
279
 
@@ -283,6 +250,10 @@ def video_stream_gen(stream_id, source, location):
283
  last_buffer = None
284
  retry_count = 0
285
 
 
 
 
 
286
  if stream_id in active_streams:
287
  active_streams[stream_id]["label"] = "Scanning Stream..."
288
 
@@ -293,20 +264,21 @@ def video_stream_gen(stream_id, source, location):
293
  force_read = False
294
 
295
  if state.get('skip_val', 0) != 0:
296
- current_frame = cap.get(cv2.CAP_PROP_POS_FRAMES)
297
- target = current_frame + (state['skip_val'] * fps)
298
- cap.set(cv2.CAP_PROP_POS_FRAMES, max(0, min(target, total_frames - 1)))
 
299
  state['skip_val'] = 0
300
- frames_3d.clear()
301
- yolo_probs.clear()
302
  force_read = True
303
 
304
  if state.get('seek_to') is not None:
305
  target = int(state['seek_to'] * total_frames)
306
  cap.set(cv2.CAP_PROP_POS_FRAMES, target)
 
 
307
  state['seek_to'] = None
308
- frames_3d.clear()
309
- yolo_probs.clear()
310
  force_read = True
311
 
312
  if state.get('force_update'):
@@ -314,59 +286,63 @@ def video_stream_gen(stream_id, source, location):
314
  state['force_update'] = False
315
 
316
  if state.get('paused') and not force_read:
 
317
  time.sleep(0.1)
318
  if last_buffer: yield (b'--frame\r\nContent-Type: image/jpeg\r\n\r\n' + last_buffer + b'\r\n')
319
  continue
320
 
321
- loop_start = time.time()
 
 
 
 
 
 
 
 
 
322
  ret, frame = cap.read()
 
323
 
324
  if not ret:
325
- if is_live:
326
  retry_count += 1
327
- if retry_count > 30: # Give up if IP cam is completely dead
328
- break
329
- time.sleep(0.2)
330
- continue
331
- elif total_frames > 0:
332
- # Auto-loop the video when it reaches the end for continuous replay
333
- cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
334
- frames_3d.clear()
335
- yolo_probs.clear()
336
  continue
337
- else:
338
- break
 
339
 
340
- retry_count = 0 # reset on success
341
-
342
- # 🚨 FIX: Prevent High-Res IP Cams from OOM Crashing HuggingFace
343
  h, w = frame.shape[:2]
344
- if w > 1280:
345
- frame = cv2.resize(frame, (1280, int(h * 1280 / w)))
346
 
347
  if total_frames > 0:
348
- state['progress'] = (cap.get(cv2.CAP_PROP_POS_FRAMES) / total_frames) * 100
349
 
350
- # AI Tracking Visualization Layer
351
  try:
352
  if state.get("show_tracking", True):
353
  track_res = model_tracker(frame, classes=[2, 3, 5, 7], verbose=False, conf=0.3)[0]
354
  display_frame = track_res.plot()
355
  else:
356
- display_frame = frame
357
- except Exception as e:
358
- display_frame = frame
359
 
360
- # Brain / Severity Scanning
361
  if not state.get("final_decision"):
362
  try:
363
  res = model_yolo(frame, verbose=False)[0]
364
-
365
  probs = np.zeros(4)
366
  if res.probs is not None:
367
  data = res.probs.data.cpu().numpy()
368
- length = min(len(data), 4)
369
- probs[:length] = data[:length]
370
  elif res.boxes is not None and len(res.boxes) > 0:
371
  for box in res.boxes:
372
  cls_id = int(box.cls[0].item())
@@ -384,23 +360,20 @@ def video_stream_gen(stream_id, source, location):
384
  p_max = np.max(yolo_probs, axis=0) if yolo_probs else np.zeros(4)
385
  p_mean = np.mean(yolo_probs, axis=0) if yolo_probs else np.zeros(4)
386
  p_min = np.min(yolo_probs, axis=0) if yolo_probs else np.zeros(4)
387
-
388
  threading.Thread(target=run_temporal_analysis, args=(
389
  stream_id, list(frames_3d), p_max, p_mean, p_min, float(np.max(p_max))*100, frame.copy(), location
390
  )).start()
391
- except Exception as e:
392
- pass # Skip frame if AI crashes
393
 
394
  _, buffer = cv2.imencode('.jpg', display_frame)
395
  last_buffer = buffer.tobytes()
396
  yield (b'--frame\r\nContent-Type: image/jpeg\r\n\r\n' + last_buffer + b'\r\n')
397
  frame_count += 1
398
 
 
399
  if not is_live:
400
- elapsed = time.time() - loop_start
401
- sleep_time = (1.0 / fps) - elapsed
402
- if sleep_time > 0:
403
- time.sleep(sleep_time)
404
 
405
  cap.release()
406
 
@@ -420,31 +393,22 @@ def predict_traffic_risk():
420
  try:
421
  if model_traffic is None: return jsonify({"error": "Tabular model missing"}), 500
422
  df = pd.DataFrame([{k: (v if v != "" else None) for k, v in data.items()}])
423
-
424
  prob = float(model_traffic.predict_proba(df)[0][1] * 100)
425
-
426
- if prob >= 70: status = "Major"
427
- elif prob >= 35: status = "Moderate"
428
- else: status = "Minor"
429
-
430
  return jsonify({"status": status, "risk_probability_percentage": prob})
431
  except Exception as e: return jsonify({"error": str(e)}), 500
432
 
433
  @app.route('/favicon.ico')
434
- @app.route('/logo.PNG')
435
  @app.route('/logo.png')
 
436
  def serve_logo():
437
- """Forces browsers to read the logo with absolute pathing to fix Hugging Face 404s and Case Sensitivity."""
438
- base_dir = os.path.dirname(os.path.abspath(__file__))
439
- static_dir = os.path.join(base_dir, 'static')
440
-
441
- # Try exact uppercase first, fallback to lowercase if Linux changed it
442
- if os.path.exists(os.path.join(static_dir, 'logo.PNG')):
443
- return send_from_directory(static_dir, 'logo.PNG', mimetype='image/png')
444
- elif os.path.exists(os.path.join(static_dir, 'logo.png')):
445
  return send_from_directory(static_dir, 'logo.png', mimetype='image/png')
446
-
447
- return "Logo not found in static directory", 404
 
448
 
449
  @app.route('/')
450
  def index(): return render_template('index.html')
 
17
  import numpy as np
18
  import pandas as pd
19
  import joblib
 
 
20
  import threading
21
  import uuid
22
  import time
23
  import requests
24
  import datetime
25
  from urllib.parse import urlparse
 
26
  from flask import Flask, request, render_template, jsonify, Response, send_from_directory
27
  from werkzeug.utils import secure_filename
28
 
 
53
 
54
  app = Flask(__name__)
55
 
56
+ # --- HUGGING FACE SAFE SOS DISPATCH ---
57
+ # We use ntfy.sh (HTTP Push) which is guaranteed to bypass Hugging Face email/SMTP blocks.
58
+ NTFY_TOPIC = "CrashVision_SOS_Alerts"
 
 
59
 
60
  UPLOAD_FOLDER = 'uploads'
61
  MODEL_FOLDER = 'models'
 
94
  # --- Helper Functions ---
95
 
96
  def get_geo_info(ip_or_url=None):
 
97
  try:
98
  target = ""
99
  if ip_or_url:
 
124
  if stream_id in active_streams:
125
  active_streams[stream_id]["plates"] = detected_plates
126
 
127
+ # --- HTTP PUSH DISPATCH (Replaces Email on Hugging Face) ---
128
  try:
129
+ _, buffer = cv2.imencode('.jpg', frame)
130
  headers = {
131
  "Title": f"🚨 {severity.upper()} ACCIDENT DETECTED",
132
+ "Priority": "5",
133
+ "Tags": "rotating_light,car",
134
+ "Click": f"https://huggingface.co/spaces/{os.environ.get('SPACE_ID', '')}"
135
  }
136
+ requests.post(f"https://ntfy.sh/{NTFY_TOPIC}", data=buffer.tobytes(), headers=headers, timeout=10)
137
+ print(f"📡 SOS Push dispatched to ntfy.sh/{NTFY_TOPIC}", flush=True)
 
138
  except Exception as e:
139
+ print(f"📡 SOS Push failed: {e}", flush=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
  def run_temporal_analysis(stream_id, frames_3d_copy, prob_max, prob_mean, prob_min, yolo_max_conf, raw_frame, location):
142
  state = active_streams.get(stream_id)
 
156
  confidence = float(ensemble_probs[final_idx] * 100)
157
  severity = classes[final_idx]
158
  except:
 
159
  final_idx = min(int(np.argmax(prob_max)), 2)
160
  confidence = float(np.max(prob_max)) * 100
161
  severity = classes[final_idx]
 
165
  state["cnn"] = round(yolo_max_conf, 1)
166
  state["rcnn"] = round(float(np.max(prob_3d)) * 100, 1)
167
 
 
168
  if confidence > 75:
169
  state["final_decision"] = True
170
  state["label"] = f"Incident Logged: {severity.capitalize()}"
 
173
  state["label"] = f"Analyzing... ({severity.capitalize()})"
174
 
175
  except Exception as e:
176
+ print(f"AI Error: {e}", flush=True)
 
177
  finally:
178
  if state: state["is_analyzing"] = False
179
 
 
199
  def init_stream():
200
  url = request.json.get('url')
201
 
202
+ # YouTube Link Extractor
203
  if 'youtube.com' in url or 'youtu.be' in url:
204
  try:
205
  ydl_opts = {'format': 'best[ext=mp4]/best/bestvideo', 'quiet': True, 'noplaylist': True}
206
  with yt_dlp.YoutubeDL(ydl_opts) as ydl:
207
  info = ydl.extract_info(url, download=False)
208
+ if 'url' in info: url = info['url']
209
+ elif 'formats' in info and len(info['formats']) > 0: url = info['formats'][-1]['url']
 
 
210
  except Exception as e:
211
  print(f"yt-dlp extraction failed: {e}", flush=True)
212
 
 
223
 
224
  @app.route('/video_control/<stream_id>', methods=['POST'])
225
  def video_control(stream_id):
 
226
  state = active_streams.get(stream_id)
227
  if not state: return jsonify({"error": "not found"}), 404
228
 
229
  action = request.json.get('action')
230
  if action == 'toggle_tracking':
231
+ state["show_tracking"] = bool(request.json.get("track"))
232
+ state["force_update"] = True
233
  elif action == 'pause': state["paused"] = True
234
  elif action == 'play': state["paused"] = False
235
  elif action == 'seek': state["seek_to"] = request.json.get("value", 0.0)
 
240
  def video_stream_gen(stream_id, source, location):
241
  cap = cv2.VideoCapture(source)
242
  fps = cap.get(cv2.CAP_PROP_FPS)
243
+ if not fps or fps == 0: fps = 25.0
244
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
245
  is_live = str(source).startswith('http')
246
 
 
250
  last_buffer = None
251
  retry_count = 0
252
 
253
+ # --- Real-Time Sync Engine (Forces original speed) ---
254
+ start_sync_time = time.time()
255
+ current_frame_idx = 0
256
+
257
  if stream_id in active_streams:
258
  active_streams[stream_id]["label"] = "Scanning Stream..."
259
 
 
264
  force_read = False
265
 
266
  if state.get('skip_val', 0) != 0:
267
+ target = max(0, min(current_frame_idx + (state['skip_val'] * fps), total_frames - 1))
268
+ cap.set(cv2.CAP_PROP_POS_FRAMES, target)
269
+ current_frame_idx = int(target)
270
+ start_sync_time = time.time() - (current_frame_idx / fps)
271
  state['skip_val'] = 0
272
+ frames_3d.clear(); yolo_probs.clear()
 
273
  force_read = True
274
 
275
  if state.get('seek_to') is not None:
276
  target = int(state['seek_to'] * total_frames)
277
  cap.set(cv2.CAP_PROP_POS_FRAMES, target)
278
+ current_frame_idx = target
279
+ start_sync_time = time.time() - (current_frame_idx / fps)
280
  state['seek_to'] = None
281
+ frames_3d.clear(); yolo_probs.clear()
 
282
  force_read = True
283
 
284
  if state.get('force_update'):
 
286
  state['force_update'] = False
287
 
288
  if state.get('paused') and not force_read:
289
+ start_sync_time = time.time() - (current_frame_idx / fps) # Freeze timeline
290
  time.sleep(0.1)
291
  if last_buffer: yield (b'--frame\r\nContent-Type: image/jpeg\r\n\r\n' + last_buffer + b'\r\n')
292
  continue
293
 
294
+ # --- Frameskipping to maintain exact 1x normal speed if AI lags ---
295
+ if not is_live and not force_read:
296
+ elapsed = time.time() - start_sync_time
297
+ target_frame = int(elapsed * fps)
298
+ frames_to_skip = target_frame - current_frame_idx
299
+ if frames_to_skip > 0:
300
+ for _ in range(min(frames_to_skip, 5)): # Cap skips to avoid locking CPU
301
+ cap.grab()
302
+ current_frame_idx += 1
303
+
304
  ret, frame = cap.read()
305
+ current_frame_idx += 1
306
 
307
  if not ret:
308
+ if is_live: # Auto-Reconnect for dropped IP Cams
309
  retry_count += 1
310
+ if retry_count > 30: break
311
+ cap.release(); time.sleep(0.5)
312
+ cap = cv2.VideoCapture(source); continue
313
+ elif total_frames > 0: # Auto-Loop
314
+ cap.set(cv2.CAP_PROP_POS_FRAMES, 0); current_frame_idx = 0
315
+ start_sync_time = time.time(); frames_3d.clear(); yolo_probs.clear()
 
 
 
316
  continue
317
+ else: break
318
+
319
+ retry_count = 0
320
 
321
+ # Resize high-res streams to prevent Hugging Face OOM Memory crashes
 
 
322
  h, w = frame.shape[:2]
323
+ if w > 1280: frame = cv2.resize(frame, (1280, int(h * 1280 / w)))
 
324
 
325
  if total_frames > 0:
326
+ state['progress'] = (current_frame_idx / total_frames) * 100
327
 
328
+ # Tracking Visualization Toggle
329
  try:
330
  if state.get("show_tracking", True):
331
  track_res = model_tracker(frame, classes=[2, 3, 5, 7], verbose=False, conf=0.3)[0]
332
  display_frame = track_res.plot()
333
  else:
334
+ display_frame = frame.copy() # Shows untouched RAW video
335
+ except:
336
+ display_frame = frame.copy()
337
 
338
+ # Severity Scanning Model
339
  if not state.get("final_decision"):
340
  try:
341
  res = model_yolo(frame, verbose=False)[0]
 
342
  probs = np.zeros(4)
343
  if res.probs is not None:
344
  data = res.probs.data.cpu().numpy()
345
+ probs[:min(len(data), 4)] = data[:min(len(data), 4)]
 
346
  elif res.boxes is not None and len(res.boxes) > 0:
347
  for box in res.boxes:
348
  cls_id = int(box.cls[0].item())
 
360
  p_max = np.max(yolo_probs, axis=0) if yolo_probs else np.zeros(4)
361
  p_mean = np.mean(yolo_probs, axis=0) if yolo_probs else np.zeros(4)
362
  p_min = np.min(yolo_probs, axis=0) if yolo_probs else np.zeros(4)
 
363
  threading.Thread(target=run_temporal_analysis, args=(
364
  stream_id, list(frames_3d), p_max, p_mean, p_min, float(np.max(p_max))*100, frame.copy(), location
365
  )).start()
366
+ except: pass
 
367
 
368
  _, buffer = cv2.imencode('.jpg', display_frame)
369
  last_buffer = buffer.tobytes()
370
  yield (b'--frame\r\nContent-Type: image/jpeg\r\n\r\n' + last_buffer + b'\r\n')
371
  frame_count += 1
372
 
373
+ # Throttling if video loop runs faster than intended FPS
374
  if not is_live:
375
+ wait = (current_frame_idx / fps) - (time.time() - start_sync_time)
376
+ if wait > 0: time.sleep(wait)
 
 
377
 
378
  cap.release()
379
 
 
393
  try:
394
  if model_traffic is None: return jsonify({"error": "Tabular model missing"}), 500
395
  df = pd.DataFrame([{k: (v if v != "" else None) for k, v in data.items()}])
 
396
  prob = float(model_traffic.predict_proba(df)[0][1] * 100)
397
+ status = "Major" if prob >= 70 else "Moderate" if prob >= 35 else "Minor"
 
 
 
 
398
  return jsonify({"status": status, "risk_probability_percentage": prob})
399
  except Exception as e: return jsonify({"error": str(e)}), 500
400
 
401
  @app.route('/favicon.ico')
 
402
  @app.route('/logo.png')
403
+ @app.route('/logo.PNG')
404
  def serve_logo():
405
+ # Bypasses Hugging Face relative pathing issues directly resolving the static folder
406
+ static_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
407
+ if os.path.exists(os.path.join(static_dir, 'logo.png')):
 
 
 
 
 
408
  return send_from_directory(static_dir, 'logo.png', mimetype='image/png')
409
+ elif os.path.exists(os.path.join(static_dir, 'logo.PNG')):
410
+ return send_from_directory(static_dir, 'logo.PNG', mimetype='image/png')
411
+ return "Logo not found", 404
412
 
413
  @app.route('/')
414
  def index(): return render_template('index.html')