BlackSpire commited on
Commit
065404e
Β·
verified Β·
1 Parent(s): 0120f4b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +50 -718
app.py CHANGED
@@ -6,24 +6,19 @@ POST /detect
6
  image - JPEG image file
7
  mobile_no - registered mobile number
8
  password - user password
9
-
10
  Flow:
11
  1. Validate mobile_no + password via checkuser.php
12
  2. Extract fcm_token from response
13
  3. Run YuNet face detection on the image
14
- 4. If face detected β†’ send FCM push notification (face alert)
15
- 5. If NO face detected β†’ send FCM push notification WITH image (motion alert)
16
- 6. Return detection result JSON
17
-
18
- GET /test
19
- Serves the built-in HTML test page
20
  """
21
 
22
- from flask import Flask, request, jsonify, render_template_string
23
  import cv2
24
  import numpy as np
25
  import requests
26
- import base64
27
  import os
28
 
29
  from huggingface_hub import hf_hub_download
@@ -35,9 +30,11 @@ from firebase_admin import credentials, messaging
35
  app = Flask(__name__)
36
 
37
  # ── Firebase initialisation ───────────────────────────────────────────────────
 
 
38
  SERVICE_ACCOUNT_FILE = os.environ.get(
39
  "GOOGLE_APPLICATION_CREDENTIALS",
40
- "service_account.json"
41
  )
42
 
43
  if not os.path.exists(SERVICE_ACCOUNT_FILE):
@@ -68,617 +65,51 @@ face_detector = cv2.FaceDetectorYN_create(
68
  )
69
 
70
 
71
- # ═════════════════════════════════════════════════════════════════════════════
72
- # HTML Test Page
73
- # ═════════════════════════════════════════════════════════════════════════════
74
-
75
- TEST_PAGE_HTML = """<!DOCTYPE html>
76
- <html lang="en">
77
- <head>
78
- <meta charset="UTF-8" />
79
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
80
- <title>ESP32-CAM Β· Test Console</title>
81
- <link rel="preconnect" href="https://fonts.googleapis.com">
82
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Syne:wght@700;800&display=swap" rel="stylesheet">
83
- <style>
84
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
85
-
86
- :root {
87
- --bg: #0a0c10;
88
- --panel: #111318;
89
- --border: #1e2230;
90
- --accent: #00e5ff;
91
- --accent2: #ff4d6d;
92
- --text: #c8d0e0;
93
- --muted: #4a5270;
94
- --success: #00e676;
95
- --warn: #ffab00;
96
- --mono: 'JetBrains Mono', monospace;
97
- --display: 'Syne', sans-serif;
98
- }
99
-
100
- body {
101
- background: var(--bg);
102
- color: var(--text);
103
- font-family: var(--mono);
104
- min-height: 100vh;
105
- display: flex;
106
- flex-direction: column;
107
- align-items: center;
108
- padding: 40px 16px 60px;
109
- }
110
-
111
- /* scanline overlay */
112
- body::before {
113
- content: '';
114
- position: fixed; inset: 0;
115
- background: repeating-linear-gradient(
116
- 0deg,
117
- transparent,
118
- transparent 2px,
119
- rgba(0,0,0,0.07) 2px,
120
- rgba(0,0,0,0.07) 4px
121
- );
122
- pointer-events: none;
123
- z-index: 999;
124
- }
125
-
126
- header {
127
- text-align: center;
128
- margin-bottom: 40px;
129
- }
130
- header .badge {
131
- display: inline-block;
132
- background: var(--accent);
133
- color: #000;
134
- font-size: 10px;
135
- font-weight: 700;
136
- letter-spacing: 2px;
137
- padding: 3px 10px;
138
- margin-bottom: 12px;
139
- clip-path: polygon(6px 0%, 100% 0%, calc(100% - 6px) 100%, 0% 100%);
140
- }
141
- header h1 {
142
- font-family: var(--display);
143
- font-size: clamp(28px, 5vw, 48px);
144
- font-weight: 800;
145
- color: #fff;
146
- letter-spacing: -1px;
147
- line-height: 1;
148
- }
149
- header h1 span { color: var(--accent); }
150
- header p {
151
- margin-top: 10px;
152
- color: var(--muted);
153
- font-size: 13px;
154
- letter-spacing: 0.5px;
155
- }
156
-
157
- .grid {
158
- display: grid;
159
- grid-template-columns: 1fr 1fr;
160
- gap: 20px;
161
- width: 100%;
162
- max-width: 900px;
163
- }
164
- @media(max-width: 680px) { .grid { grid-template-columns: 1fr; } }
165
-
166
- .card {
167
- background: var(--panel);
168
- border: 1px solid var(--border);
169
- border-radius: 4px;
170
- padding: 24px;
171
- position: relative;
172
- }
173
- .card::before {
174
- content: '';
175
- position: absolute;
176
- top: 0; left: 0; right: 0;
177
- height: 2px;
178
- background: linear-gradient(90deg, var(--accent), transparent);
179
- }
180
- .card.red::before { background: linear-gradient(90deg, var(--accent2), transparent); }
181
-
182
- .card h2 {
183
- font-family: var(--display);
184
- font-size: 14px;
185
- font-weight: 700;
186
- letter-spacing: 2px;
187
- text-transform: uppercase;
188
- color: #fff;
189
- margin-bottom: 20px;
190
- display: flex;
191
- align-items: center;
192
- gap: 8px;
193
- }
194
- .card h2 .dot {
195
- width: 8px; height: 8px;
196
- border-radius: 50%;
197
- background: var(--accent);
198
- box-shadow: 0 0 8px var(--accent);
199
- animation: pulse 2s infinite;
200
- }
201
- .card.red h2 .dot { background: var(--accent2); box-shadow: 0 0 8px var(--accent2); }
202
-
203
- @keyframes pulse {
204
- 0%,100% { opacity: 1; }
205
- 50% { opacity: 0.4; }
206
- }
207
-
208
- label {
209
- display: block;
210
- font-size: 11px;
211
- letter-spacing: 1.5px;
212
- color: var(--muted);
213
- text-transform: uppercase;
214
- margin-bottom: 6px;
215
- margin-top: 14px;
216
- }
217
- label:first-of-type { margin-top: 0; }
218
-
219
- input[type="text"],
220
- input[type="password"],
221
- input[type="file"] {
222
- width: 100%;
223
- background: #0d0f14;
224
- border: 1px solid var(--border);
225
- border-radius: 3px;
226
- color: var(--text);
227
- font-family: var(--mono);
228
- font-size: 13px;
229
- padding: 10px 12px;
230
- outline: none;
231
- transition: border-color 0.2s;
232
- }
233
- input:focus { border-color: var(--accent); }
234
-
235
- input[type="file"] {
236
- padding: 8px 10px;
237
- cursor: pointer;
238
- }
239
- input[type="file"]::file-selector-button {
240
- background: var(--border);
241
- border: none;
242
- color: var(--accent);
243
- font-family: var(--mono);
244
- font-size: 11px;
245
- letter-spacing: 1px;
246
- text-transform: uppercase;
247
- padding: 5px 12px;
248
- border-radius: 2px;
249
- cursor: pointer;
250
- margin-right: 10px;
251
- }
252
-
253
- .btn {
254
- display: flex;
255
- align-items: center;
256
- justify-content: center;
257
- gap: 8px;
258
- width: 100%;
259
- margin-top: 20px;
260
- padding: 12px;
261
- background: transparent;
262
- border: 1px solid var(--accent);
263
- color: var(--accent);
264
- font-family: var(--mono);
265
- font-size: 12px;
266
- font-weight: 700;
267
- letter-spacing: 2px;
268
- text-transform: uppercase;
269
- border-radius: 3px;
270
- cursor: pointer;
271
- transition: background 0.2s, color 0.2s;
272
- clip-path: polygon(8px 0%, 100% 0%, calc(100% - 8px) 100%, 0% 100%);
273
- }
274
- .btn:hover { background: var(--accent); color: #000; }
275
- .btn:disabled { opacity: 0.4; cursor: not-allowed; }
276
- .btn.red { border-color: var(--accent2); color: var(--accent2); }
277
- .btn.red:hover { background: var(--accent2); color: #000; }
278
-
279
- .spinner {
280
- width: 14px; height: 14px;
281
- border: 2px solid currentColor;
282
- border-top-color: transparent;
283
- border-radius: 50%;
284
- animation: spin 0.7s linear infinite;
285
- display: none;
286
- }
287
- @keyframes spin { to { transform: rotate(360deg); } }
288
-
289
- /* image preview */
290
- #imagePreview {
291
- display: none;
292
- margin-top: 12px;
293
- border: 1px solid var(--border);
294
- border-radius: 3px;
295
- overflow: hidden;
296
- max-height: 180px;
297
- }
298
- #imagePreview img {
299
- width: 100%;
300
- height: 180px;
301
- object-fit: cover;
302
- display: block;
303
- opacity: 0.85;
304
- }
305
-
306
- /* response log */
307
- .log-card {
308
- grid-column: 1 / -1;
309
- }
310
- #logBox {
311
- background: #07090d;
312
- border: 1px solid var(--border);
313
- border-radius: 3px;
314
- padding: 16px;
315
- min-height: 160px;
316
- max-height: 320px;
317
- overflow-y: auto;
318
- font-size: 12px;
319
- line-height: 1.8;
320
- }
321
- .log-entry { border-bottom: 1px solid #1a1d28; padding-bottom: 10px; margin-bottom: 10px; }
322
- .log-entry:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
323
- .log-time { color: var(--muted); font-size: 10px; }
324
- .log-tag {
325
- display: inline-block;
326
- padding: 1px 6px;
327
- border-radius: 2px;
328
- font-size: 10px;
329
- font-weight: 700;
330
- letter-spacing: 1px;
331
- margin-right: 6px;
332
- }
333
- .tag-ok { background: #002a1a; color: var(--success); }
334
- .tag-err { background: #2a0010; color: var(--accent2); }
335
- .tag-info{ background: #001a2a; color: var(--accent); }
336
- .tag-warn{ background: #2a1a00; color: var(--warn); }
337
-
338
- .log-json {
339
- color: #7eb8d4;
340
- white-space: pre-wrap;
341
- word-break: break-all;
342
- margin-top: 4px;
343
- font-size: 11px;
344
- }
345
- .empty-log { color: var(--muted); font-size: 12px; }
346
-
347
- /* status bar */
348
- .status-bar {
349
- width: 100%; max-width: 900px;
350
- display: flex;
351
- align-items: center;
352
- gap: 8px;
353
- margin-bottom: 16px;
354
- font-size: 11px;
355
- color: var(--muted);
356
- letter-spacing: 1px;
357
- }
358
- .status-bar .indicator {
359
- width: 6px; height: 6px;
360
- border-radius: 50%;
361
- background: var(--success);
362
- box-shadow: 0 0 6px var(--success);
363
- }
364
- </style>
365
- </head>
366
- <body>
367
-
368
- <header>
369
- <div class="badge">ESP32-CAM Β· SECURITY SYSTEM</div>
370
- <h1>DETECTION <span>CONSOLE</span></h1>
371
- <p>Test face detection &amp; push notifications Β· Hosted on Hugging Face</p>
372
- </header>
373
-
374
- <div class="status-bar">
375
- <div class="indicator"></div>
376
- <span>API ONLINE</span>
377
- <span style="margin-left:auto; opacity:0.5" id="clock"></span>
378
- </div>
379
-
380
- <div class="grid">
381
-
382
- <!-- Detect Card -->
383
- <div class="card">
384
- <h2><span class="dot"></span>Face Detect + Notify</h2>
385
-
386
- <label>Mobile Number</label>
387
- <input type="text" id="detectMobile" placeholder="+91XXXXXXXXXX" />
388
-
389
- <label>Password</label>
390
- <input type="password" id="detectPassword" placeholder="β€’β€’β€’β€’β€’β€’β€’β€’" />
391
-
392
- <label>Camera Image (JPEG)</label>
393
- <input type="file" id="detectImage" accept="image/*" />
394
- <div id="imagePreview"><img id="previewImg" src="" alt="preview"/></div>
395
-
396
- <button class="btn" id="detectBtn" onclick="runDetect()">
397
- <div class="spinner" id="detectSpinner"></div>
398
- <span>β–Ά RUN DETECTION</span>
399
- </button>
400
- </div>
401
-
402
- <!-- Test Notify Card -->
403
- <div class="card red">
404
- <h2><span class="dot"></span>Send Test Notification</h2>
405
-
406
- <label>Mobile Number</label>
407
- <input type="text" id="testMobile" placeholder="+91XXXXXXXXXX" />
408
-
409
- <label>Password</label>
410
- <input type="password" id="testPassword" placeholder="β€’β€’β€’β€’β€’β€’β€’β€’" />
411
-
412
- <label>Custom Message (optional)</label>
413
- <input type="text" id="testMessage" placeholder="Test alert from console" />
414
-
415
- <button class="btn red" id="testBtn" onclick="sendTest()">
416
- <div class="spinner" id="testSpinner"></div>
417
- <span>πŸ”” SEND TEST PUSH</span>
418
- </button>
419
- </div>
420
-
421
- <!-- Debug Auth Card -->
422
- <div class="card" style="grid-column: 1 / -1; border-color: #2a1a00;">
423
- <h2 style="color:#ffab00"><span class="dot" style="background:#ffab00;box-shadow:0 0 8px #ffab00"></span>Debug Auth (Troubleshoot Login)</h2>
424
- <p style="font-size:11px;color:var(--muted);margin-bottom:14px;letter-spacing:.5px">
425
- Calls checkuser.php directly and shows the raw response β€” use this to diagnose credential issues.
426
- </p>
427
- <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;align-items:end">
428
- <div>
429
- <label>Mobile Number</label>
430
- <input type="text" id="dbgMobile" placeholder="+91XXXXXXXXXX" />
431
- </div>
432
- <div>
433
- <label>Password</label>
434
- <input type="password" id="dbgPassword" placeholder="β€’β€’β€’β€’β€’β€’β€’β€’" />
435
- </div>
436
- <button class="btn" style="border-color:#ffab00;color:#ffab00;margin-top:0" id="dbgBtn" onclick="runDebug()">
437
- <div class="spinner" id="dbgSpinner"></div>
438
- <span>πŸ” TEST AUTH</span>
439
- </button>
440
- </div>
441
- <div id="dbgResult" style="display:none;margin-top:14px;background:#07090d;border:1px solid #2a1a00;border-radius:3px;padding:14px;font-size:11px;line-height:1.8;color:#7eb8d4;white-space:pre-wrap;word-break:break-all"></div>
442
- </div>
443
-
444
- <!-- Log Card -->
445
- <div class="card log-card">
446
- <h2><span class="dot"></span>Response Log</h2>
447
- <div id="logBox">
448
- <div class="empty-log">[ Waiting for requests... ]</div>
449
- </div>
450
- </div>
451
-
452
- </div>
453
-
454
- <script>
455
- // ── Clock ────────────────────────────────────────────────────────────────
456
- const clk = document.getElementById('clock');
457
- setInterval(() => {
458
- clk.textContent = new Date().toLocaleTimeString('en-IN', { hour12: false });
459
- }, 1000);
460
-
461
- // ── Image preview ────────────────────────────────────────────────────────
462
- document.getElementById('detectImage').addEventListener('change', function() {
463
- const file = this.files[0];
464
- if (!file) return;
465
- const reader = new FileReader();
466
- reader.onload = e => {
467
- document.getElementById('previewImg').src = e.target.result;
468
- document.getElementById('imagePreview').style.display = 'block';
469
- };
470
- reader.readAsDataURL(file);
471
- });
472
-
473
- // ── Log helper ────────────────────────────────────────────────────────────
474
- function log(tagClass, tagText, message, json) {
475
- const box = document.getElementById('logBox');
476
- const empty = box.querySelector('.empty-log');
477
- if (empty) empty.remove();
478
-
479
- const time = new Date().toLocaleTimeString('en-IN', { hour12: false });
480
- const entry = document.createElement('div');
481
- entry.className = 'log-entry';
482
- entry.innerHTML = `
483
- <div class="log-time">${time}</div>
484
- <span class="log-tag ${tagClass}">${tagText}</span>
485
- <span>${message}</span>
486
- ${json ? `<div class="log-json">${JSON.stringify(json, null, 2)}</div>` : ''}
487
- `;
488
- box.prepend(entry);
489
- }
490
-
491
- // ── Spinner toggle ─────────────────────────────────────────────────────
492
- function setBusy(btnId, spinnerId, busy) {
493
- document.getElementById(btnId).disabled = busy;
494
- document.getElementById(spinnerId).style.display = busy ? 'block' : 'none';
495
- }
496
-
497
- // ── Run Detection ─────────────────────────────────────────────────────────
498
- async function runDetect() {
499
- const mobile = document.getElementById('detectMobile').value.trim();
500
- const password = document.getElementById('detectPassword').value.trim();
501
- const imageFile = document.getElementById('detectImage').files[0];
502
-
503
- if (!mobile || !password) { log('tag-err', 'ERROR', 'Mobile number and password are required.'); return; }
504
- if (!imageFile) { log('tag-err', 'ERROR', 'Please select an image file.'); return; }
505
-
506
- setBusy('detectBtn', 'detectSpinner', true);
507
- log('tag-info', 'INFO', `Sending image: ${imageFile.name} (${(imageFile.size/1024).toFixed(1)} KB)`);
508
-
509
- const form = new FormData();
510
- form.append('mobile_no', mobile);
511
- form.append('password', password);
512
- form.append('image', imageFile);
513
-
514
- try {
515
- const res = await fetch('/detect', { method: 'POST', body: form });
516
- const data = await res.json();
517
- if (data.success) {
518
- const hasF = data.has_face;
519
- log(
520
- hasF ? 'tag-ok' : 'tag-warn',
521
- hasF ? 'FACE' : 'MOTION',
522
- data.message,
523
- data
524
- );
525
- } else {
526
- log('tag-err', 'FAIL', data.error || 'Unknown error', data);
527
- }
528
- } catch(e) {
529
- log('tag-err', 'ERROR', `Network error: ${e.message}`);
530
- } finally {
531
- setBusy('detectBtn', 'detectSpinner', false);
532
- }
533
- }
534
-
535
- // ── Debug Auth ────────────────────────────────────────────────────────────
536
- async function runDebug() {
537
- const mobile = document.getElementById('dbgMobile').value.trim();
538
- const password = document.getElementById('dbgPassword').value.trim();
539
- if (!mobile || !password) { log('tag-err', 'ERROR', 'Fill mobile and password in the debug panel.'); return; }
540
-
541
- setBusy('dbgBtn', 'dbgSpinner', true);
542
- const box = document.getElementById('dbgResult');
543
- box.style.display = 'block';
544
- box.textContent = 'Calling checkuser.php...';
545
-
546
- try {
547
- const res = await fetch('/debug-auth', {
548
- method: 'POST',
549
- headers: {'Content-Type': 'application/json'},
550
- body: JSON.stringify({mobile_no: mobile, password})
551
- });
552
- const data = await res.json();
553
- box.textContent = JSON.stringify(data, null, 2);
554
- log('tag-warn', 'DEBUG', 'Raw PHP auth response received β€” see debug panel above', data);
555
- } catch(e) {
556
- box.textContent = 'Error: ' + e.message;
557
- } finally {
558
- setBusy('dbgBtn', 'dbgSpinner', false);
559
- }
560
- }
561
-
562
- // ── Send Test Notification ────────────────────────────────────────────────
563
- async function sendTest() {
564
- const mobile = document.getElementById('testMobile').value.trim();
565
- const password = document.getElementById('testPassword').value.trim();
566
- const message = document.getElementById('testMessage').value.trim();
567
-
568
- if (!mobile || !password) { log('tag-err', 'ERROR', 'Mobile number and password are required.'); return; }
569
-
570
- setBusy('testBtn', 'testSpinner', true);
571
- log('tag-info', 'INFO', `Sending test notification to ${mobile}...`);
572
-
573
- try {
574
- const res = await fetch('/test-notify', {
575
- method: 'POST',
576
- headers: { 'Content-Type': 'application/json' },
577
- body: JSON.stringify({ mobile_no: mobile, password, message })
578
- });
579
- const data = await res.json();
580
- if (data.success) {
581
- log('tag-ok', 'SENT', data.message, data);
582
- } else {
583
- log('tag-err', 'FAIL', data.error || 'Unknown error', data);
584
- }
585
- } catch(e) {
586
- log('tag-err', 'ERROR', `Network error: ${e.message}`);
587
- } finally {
588
- setBusy('testBtn', 'testSpinner', false);
589
- }
590
- }
591
- </script>
592
- </body>
593
- </html>"""
594
-
595
-
596
  # ═════════════════════════════════════════════════════════════════════════════
597
  # Helpers
598
  # ═════════════════════════════════════════════════════��═══════════════════════
599
 
600
  def verify_user(mobile_no: str, password: str) -> dict:
601
  """
602
- Call checkuser.php with mobile_no + password (JSON body).
603
  Returns the parsed JSON dict on success, raises ValueError on failure.
604
  """
605
- payload = {"mobile_no": mobile_no, "password": password}
606
  try:
607
  resp = requests.post(
608
  CHECKUSER_URL,
609
- json=payload,
610
  timeout=10
611
  )
612
  except requests.RequestException as e:
613
  raise ValueError(f"Could not reach auth server: {e}")
614
 
615
- raw_text = resp.text # keep for debug
616
- print(f"[AUTH] HTTP {resp.status_code} | raw: {raw_text[:300]}")
617
-
618
  try:
619
  data = resp.json()
620
- except Exception:
621
- raise ValueError(f"Auth server returned non-JSON: {raw_text[:200]}")
622
 
623
  if resp.status_code != 200 or not data.get("success"):
624
- msg = data.get("message") or data.get("error") or f"HTTP {resp.status_code}"
625
- raise ValueError(msg)
626
 
627
  return data
628
 
629
 
630
- def image_to_base64_jpeg(img_bytes: bytes) -> str:
631
- """Convert raw image bytes to a base64-encoded JPEG data URI."""
632
- encoded = base64.b64encode(img_bytes).decode("utf-8")
633
- return f"data:image/jpeg;base64,{encoded}"
634
-
635
-
636
- def send_fcm_notification(
637
- fcm_token: str,
638
- face_count: int,
639
- image_bytes: bytes = None
640
- ):
641
  """
642
  Send a push notification via Firebase Cloud Messaging (FCM v1 API).
643
-
644
- - face_count > 0 β†’ face alert (no image needed, ESP32 already sends it)
645
- - face_count == 0 β†’ motion alert WITH image as data payload
646
  """
647
  if not fcm_token:
648
  print("[FCM] No FCM token available β€” skipping notification")
649
  return
650
 
651
- if face_count > 0:
652
- # ── Face detected notification ────────────────────────────────────────
653
- title = "🚨 Face Alert"
654
- body = (
655
- f"{face_count} face{'s' if face_count != 1 else ''} "
656
- f"detected by your ESP32-CAM!"
657
- )
658
- data_payload = {"type": "face_detected", "count": str(face_count)}
659
- else:
660
- # ── Motion (no face) notification β€” attach image as base64 ────────────
661
- title = "⚠️ Motion Detected"
662
- body = "Movement detected but no face found. Check the snapshot."
663
- data_payload = {"type": "motion_detected", "count": "0"}
664
-
665
- # Include image only if provided (keeps payload under FCM 4 KB limit)
666
- if image_bytes:
667
- b64 = base64.b64encode(image_bytes).decode("utf-8")
668
- # Trim if over ~3.5 KB to stay safely under FCM data limit
669
- if len(b64) > 3500:
670
- # Send thumbnail instead: resize via OpenCV first
671
- np_arr = np.frombuffer(image_bytes, np.uint8)
672
- img = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
673
- if img is not None:
674
- thumb = cv2.resize(img, (160, 120))
675
- _, buf = cv2.imencode(".jpg", thumb, [cv2.IMWRITE_JPEG_QUALITY, 60])
676
- b64 = base64.b64encode(buf.tobytes()).decode("utf-8")
677
- data_payload["image_b64"] = b64
678
- print(f"[FCM] Motion image payload size: {len(b64)} chars")
679
-
680
  message = messaging.Message(
681
- notification=messaging.Notification(title=title, body=body),
 
 
 
 
 
682
  android=messaging.AndroidConfig(
683
  priority="high",
684
  notification=messaging.AndroidNotification(
@@ -691,14 +122,14 @@ def send_fcm_notification(
691
  aps=messaging.Aps(sound="default")
692
  )
693
  ),
694
- data=data_payload,
695
  token=fcm_token,
696
  )
697
 
698
  try:
699
  response = messaging.send(message)
700
- print(f"[FCM] Notification sent. Message ID: {response}")
701
  except Exception as e:
 
702
  print(f"[FCM] Failed to send notification: {e}")
703
 
704
 
@@ -707,22 +138,17 @@ def send_fcm_notification(
707
  # ═════════════════════════════════════════════════════════════════════════════
708
 
709
  @app.route("/", methods=["GET"])
710
- @app.route("/test", methods=["GET"])
711
  def home():
712
- """Serve the HTML test console on both / and /test."""
713
- return render_template_string(TEST_PAGE_HTML)
714
-
715
-
716
- @app.route("/api", methods=["GET"])
717
- def api_info():
718
- """JSON info endpoint."""
719
  return jsonify({
720
  "status": "API is running",
721
- "endpoints": {
722
- "GET /": "HTML test console page",
723
- "GET /test": "HTML test console page",
724
- "POST /detect": "Face detection + FCM notification",
725
- "POST /test-notify": "Send a test push notification"
 
 
 
726
  }
727
  })
728
 
@@ -741,21 +167,30 @@ def detect():
741
  }), 400
742
 
743
  if "image" not in request.files:
744
- return jsonify({"success": False, "error": "No 'image' file in request"}), 400
 
 
 
745
 
746
  image_file = request.files["image"]
747
  if image_file.filename == "":
748
- return jsonify({"success": False, "error": "No file selected"}), 400
 
 
 
749
 
750
  # ── 2. Authenticate user ─────────────────────────────────────────────────
751
  try:
752
  user_data = verify_user(mobile_no, password)
753
  except ValueError as e:
754
- return jsonify({"success": False, "error": str(e)}), 401
 
 
 
755
 
756
  fcm_token = user_data.get("fcm_token") or ""
757
  user_name = user_data.get("name", mobile_no)
758
- print(f"[AUTH] Verified user: {user_name} | FCM token: {'yes' if fcm_token else 'no'}")
759
 
760
  # ── 3. Decode image ──────────────────────────────────────────────────────
761
  file_bytes = image_file.read()
@@ -763,7 +198,10 @@ def detect():
763
  img = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
764
 
765
  if img is None:
766
- return jsonify({"success": False, "error": "Invalid or corrupted image"}), 400
 
 
 
767
 
768
  # ── 4. Run face detection ────────────────────────────────────────────────
769
  h, w = img.shape[:2]
@@ -775,125 +213,19 @@ def detect():
775
 
776
  print(f"[DETECT] has_face={has_face} count={face_count}")
777
 
778
- # ── 5. Send FCM notification ─────────────────────────────────────────────
779
- # Face detected β†’ face alert (no image payload needed)
780
- # No face found β†’ motion alert WITH the image
781
  if has_face:
782
  send_fcm_notification(fcm_token, face_count)
783
- else:
784
- send_fcm_notification(fcm_token, 0, image_bytes=file_bytes)
785
 
786
  # ── 6. Return result ─────────────────────────────────────────────────────
787
  return jsonify({
788
  "success": True,
789
  "has_face": has_face,
790
  "count": face_count,
791
- "message": (
792
- f"Face detected ({face_count} face{'s' if face_count != 1 else ''}) β€” face alert sent"
793
- if has_face else
794
- "No face detected β€” motion alert with image sent"
795
- )
796
  })
797
 
798
 
799
- @app.route("/test-notify", methods=["POST"])
800
- def test_notify():
801
- """
802
- Send a test push notification to any registered user.
803
- JSON body: { mobile_no, password, message (optional) }
804
- """
805
- body_data = request.get_json(silent=True) or {}
806
- mobile_no = body_data.get("mobile_no", "").strip()
807
- password = body_data.get("password", "").strip()
808
- custom_msg = body_data.get("message", "").strip() or "This is a test notification from your ESP32-CAM system."
809
-
810
- if not mobile_no or not password:
811
- return jsonify({
812
- "success": False,
813
- "error": "Missing 'mobile_no' or 'password'"
814
- }), 400
815
-
816
- try:
817
- user_data = verify_user(mobile_no, password)
818
- except ValueError as e:
819
- return jsonify({"success": False, "error": str(e)}), 401
820
-
821
- fcm_token = user_data.get("fcm_token") or ""
822
- user_name = user_data.get("name", mobile_no)
823
-
824
- if not fcm_token:
825
- return jsonify({
826
- "success": False,
827
- "error": f"No FCM token found for user '{user_name}'"
828
- }), 422
829
-
830
- message = messaging.Message(
831
- notification=messaging.Notification(
832
- title="πŸ”” Test Notification",
833
- body=custom_msg,
834
- ),
835
- android=messaging.AndroidConfig(
836
- priority="high",
837
- notification=messaging.AndroidNotification(
838
- sound="default",
839
- channel_id="face_detection_alerts"
840
- ),
841
- ),
842
- apns=messaging.APNSConfig(
843
- payload=messaging.APNSPayload(
844
- aps=messaging.Aps(sound="default")
845
- )
846
- ),
847
- data={"type": "test"},
848
- token=fcm_token,
849
- )
850
-
851
- try:
852
- fcm_response = messaging.send(message)
853
- return jsonify({
854
- "success": True,
855
- "message": f"Test notification sent to {user_name}",
856
- "message_id": fcm_response
857
- })
858
- except Exception as e:
859
- return jsonify({
860
- "success": False,
861
- "error": f"FCM send failed: {e}"
862
- }), 500
863
-
864
-
865
- @app.route("/debug-auth", methods=["POST"])
866
- def debug_auth():
867
- """
868
- Debug endpoint β€” shows exactly what checkuser.php returns.
869
- JSON body: { mobile_no, password }
870
- REMOVE THIS ROUTE IN PRODUCTION.
871
- """
872
- body_data = request.get_json(silent=True) or {}
873
- mobile_no = body_data.get("mobile_no", "").strip()
874
- password = body_data.get("password", "").strip()
875
-
876
- if not mobile_no or not password:
877
- return jsonify({"error": "Send mobile_no and password in JSON body"}), 400
878
-
879
- payload = {"mobile_no": mobile_no, "password": password}
880
- try:
881
- resp = requests.post(CHECKUSER_URL, json=payload, timeout=10)
882
- try:
883
- php_json = resp.json()
884
- except Exception:
885
- php_json = None
886
-
887
- return jsonify({
888
- "http_status": resp.status_code,
889
- "php_raw_text": resp.text[:500],
890
- "php_json": php_json,
891
- "sent_payload": payload,
892
- "checkuser_url": CHECKUSER_URL,
893
- })
894
- except Exception as e:
895
- return jsonify({"error": str(e)}), 500
896
-
897
-
898
  if __name__ == "__main__":
899
  app.run(host="0.0.0.0", port=7860, debug=True)
 
6
  image - JPEG image file
7
  mobile_no - registered mobile number
8
  password - user password
 
9
  Flow:
10
  1. Validate mobile_no + password via checkuser.php
11
  2. Extract fcm_token from response
12
  3. Run YuNet face detection on the image
13
+ 4. If face detected β†’ send FCM push notification to user's device
14
+ 5. Return detection result JSON
 
 
 
 
15
  """
16
 
17
+ from flask import Flask, request, jsonify
18
  import cv2
19
  import numpy as np
20
  import requests
21
+ import json
22
  import os
23
 
24
  from huggingface_hub import hf_hub_download
 
30
  app = Flask(__name__)
31
 
32
  # ── Firebase initialisation ───────────────────────────────────────────────────
33
+ # Place your service account JSON file next to this script and set the env var,
34
+ # OR just hardcode the filename here.
35
  SERVICE_ACCOUNT_FILE = os.environ.get(
36
  "GOOGLE_APPLICATION_CREDENTIALS",
37
+ "service_account.json" # ← put your Firebase service account JSON here
38
  )
39
 
40
  if not os.path.exists(SERVICE_ACCOUNT_FILE):
 
65
  )
66
 
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  # ═════════════════════════════════════════════════════════════════════════════
69
  # Helpers
70
  # ═════════════════════════════════════════════════════��═══════════════════════
71
 
72
  def verify_user(mobile_no: str, password: str) -> dict:
73
  """
74
+ Call checkuser.php with mobile_no + password.
75
  Returns the parsed JSON dict on success, raises ValueError on failure.
76
  """
 
77
  try:
78
  resp = requests.post(
79
  CHECKUSER_URL,
80
+ json={"mobile_no": mobile_no, "password": password},
81
  timeout=10
82
  )
83
  except requests.RequestException as e:
84
  raise ValueError(f"Could not reach auth server: {e}")
85
 
 
 
 
86
  try:
87
  data = resp.json()
88
+ except ValueError:
89
+ raise ValueError("Auth server returned non-JSON response")
90
 
91
  if resp.status_code != 200 or not data.get("success"):
92
+ raise ValueError(data.get("message", "Authentication failed"))
 
93
 
94
  return data
95
 
96
 
97
+ def send_fcm_notification(fcm_token: str, face_count: int):
 
 
 
 
 
 
 
 
 
 
98
  """
99
  Send a push notification via Firebase Cloud Messaging (FCM v1 API).
100
+ Only called when at least one face is detected.
 
 
101
  """
102
  if not fcm_token:
103
  print("[FCM] No FCM token available β€” skipping notification")
104
  return
105
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  message = messaging.Message(
107
+ notification=messaging.Notification(
108
+ title="Motion Alert 🚨",
109
+ body=(
110
+ f"{face_count} face{'s' if face_count != 1 else ''} detected by your ESP32-CAM!"
111
+ ),
112
+ ),
113
  android=messaging.AndroidConfig(
114
  priority="high",
115
  notification=messaging.AndroidNotification(
 
122
  aps=messaging.Aps(sound="default")
123
  )
124
  ),
 
125
  token=fcm_token,
126
  )
127
 
128
  try:
129
  response = messaging.send(message)
130
+ print(f"[FCM] Notification sent successfully. Message ID: {response}")
131
  except Exception as e:
132
+ # Log but don't crash the API β€” detection result still returns to client
133
  print(f"[FCM] Failed to send notification: {e}")
134
 
135
 
 
138
  # ═════════════════════════════════════════════════════════════════════════════
139
 
140
  @app.route("/", methods=["GET"])
 
141
  def home():
 
 
 
 
 
 
 
142
  return jsonify({
143
  "status": "API is running",
144
+ "usage": (
145
+ "POST to /detect with multipart/form-data: "
146
+ "fields 'mobile_no', 'password', and file 'image' (JPEG)"
147
+ ),
148
+ "response": {
149
+ "has_face": "true/false",
150
+ "count": "number of faces",
151
+ "message": "human-readable result"
152
  }
153
  })
154
 
 
167
  }), 400
168
 
169
  if "image" not in request.files:
170
+ return jsonify({
171
+ "success": False,
172
+ "error": "No 'image' file in request"
173
+ }), 400
174
 
175
  image_file = request.files["image"]
176
  if image_file.filename == "":
177
+ return jsonify({
178
+ "success": False,
179
+ "error": "No file selected"
180
+ }), 400
181
 
182
  # ── 2. Authenticate user ─────────────────────────────────────────────────
183
  try:
184
  user_data = verify_user(mobile_no, password)
185
  except ValueError as e:
186
+ return jsonify({
187
+ "success": False,
188
+ "error": str(e)
189
+ }), 401
190
 
191
  fcm_token = user_data.get("fcm_token") or ""
192
  user_name = user_data.get("name", mobile_no)
193
+ print(f"[AUTH] Verified user: {user_name} | FCM token present: {bool(fcm_token)}")
194
 
195
  # ── 3. Decode image ──────────────────────────────────────────────────────
196
  file_bytes = image_file.read()
 
198
  img = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
199
 
200
  if img is None:
201
+ return jsonify({
202
+ "success": False,
203
+ "error": "Invalid or corrupted image"
204
+ }), 400
205
 
206
  # ── 4. Run face detection ────────────────────────────────────────────────
207
  h, w = img.shape[:2]
 
213
 
214
  print(f"[DETECT] has_face={has_face} count={face_count}")
215
 
216
+ # ── 5. Send FCM notification if face detected ────────────────────────────
 
 
217
  if has_face:
218
  send_fcm_notification(fcm_token, face_count)
 
 
219
 
220
  # ── 6. Return result ─────────────────────────────────────────────────────
221
  return jsonify({
222
  "success": True,
223
  "has_face": has_face,
224
  "count": face_count,
225
+ "message": f"Face detected ({face_count} face{'s' if face_count != 1 else ''})"
226
+ if has_face else "No face detected"
 
 
 
227
  })
228
 
229
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  if __name__ == "__main__":
231
  app.run(host="0.0.0.0", port=7860, debug=True)