lifedebugger commited on
Commit
2cf45f4
·
1 Parent(s): bfa6bbb

Deploy files from GitHub repository with LFS

Browse files
.gitignore CHANGED
@@ -1,2 +1,5 @@
1
  ./face_db
2
  myenv/
 
 
 
 
1
  ./face_db
2
  myenv/
3
+ facerecog/
4
+ test_verify-image.py
5
+ test_verify.py
__pycache__/main.cpython-312.pyc ADDED
Binary file (19.9 kB). View file
 
__pycache__/main.cpython-313.pyc ADDED
Binary file (19.8 kB). View file
 
face_db/_live/29.jpg ADDED

Git LFS Details

  • SHA256: 1de5b1db9907c59f3b2ab7ab92cd11f47d7c0a70b7dfb9183de526acb80040c3
  • Pointer size: 131 Bytes
  • Size of remote file: 127 kB
face_db/_live/8.jpg CHANGED

Git LFS Details

  • SHA256: 4f5895d8876d1e0ba0db2da66b9e02d46ec034ea7ae0e24f21716c66a5641ad7
  • Pointer size: 130 Bytes
  • Size of remote file: 23.5 kB

Git LFS Details

  • SHA256: 1de5b1db9907c59f3b2ab7ab92cd11f47d7c0a70b7dfb9183de526acb80040c3
  • Pointer size: 131 Bytes
  • Size of remote file: 127 kB
main.py CHANGED
@@ -2,7 +2,7 @@ import os
2
  import base64
3
  import asyncio
4
  import json
5
- from typing import List, Optional, Iterable
6
 
7
  import numpy as np
8
  import cv2
@@ -12,246 +12,464 @@ from starlette.concurrency import run_in_threadpool
12
  from starlette.middleware.cors import CORSMiddleware
13
  from fastapi.responses import JSONResponse
14
 
 
15
  from uniface import RetinaFace, ArcFace
16
 
17
- # =========================
18
- # CONFIG
19
- # =========================
20
- DETECTION_THRESHOLD = 0.70
21
- SIM_THRESHOLD = 0.40
22
 
23
- FACE_DB_ROOT = "face_db"
24
- LIVE_DB_ROOT = os.path.join(FACE_DB_ROOT, "_live")
 
 
 
 
 
 
25
 
 
 
 
 
 
 
 
 
 
26
  os.makedirs(FACE_DB_ROOT, exist_ok=True)
 
 
27
  os.makedirs(LIVE_DB_ROOT, exist_ok=True)
28
 
29
  processing_lock = asyncio.Lock()
30
 
31
- # =========================
32
- # INIT MODELS
33
- # =========================
34
  detector = None
35
  recognizer = None
36
 
37
  try:
38
- print("[Uniface] Initializing RetinaFace...")
39
- detector = RetinaFace()
40
- print("[Uniface] Initializing ArcFace...")
41
  recognizer = ArcFace()
42
  except Exception as e:
43
- print("[Uniface] Init error:", e)
44
 
45
- # =========================
46
- # FASTAPI
47
- # =========================
48
- app = FastAPI(
49
- title="Face Verification API (Uniface Fixed)",
50
- version="2.3.0",
51
- )
52
 
53
- app.add_middleware(
54
- CORSMiddleware,
55
- allow_origins=["*"],
56
- allow_credentials=True,
57
- allow_methods=["*"],
58
- allow_headers=["*"],
59
- )
60
-
61
- # =========================
62
- # SCHEMAS
63
- # =========================
64
  class EnrollRequest(BaseModel):
65
- employee_id: str = "1"
66
- images: List[str]
67
 
68
  class VerifyImageRequest(BaseModel):
69
- employee_id: str = "1"
70
- image: str
71
- threshold: Optional[float] = None
72
 
73
  class ClearRequest(BaseModel):
74
- employee_id: str = "1"
 
75
 
76
- # =========================
77
- # HELPERS
78
- # =========================
79
  def _strip_b64(s: str) -> str:
80
- if "," in s and s.lower().startswith("data:"):
81
  return s.split(",", 1)[1]
82
  return s
83
 
84
- def _b64_to_bgr(b64: str):
85
  try:
86
  raw = base64.b64decode(_strip_b64(b64), validate=False)
87
- return cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR)
 
88
  except Exception:
89
  return None
90
 
91
- def _bytes_to_bgr(data: bytes):
92
  try:
93
- return cv2.imdecode(np.frombuffer(data, np.uint8), cv2.IMREAD_COLOR)
 
94
  except Exception:
95
  return None
96
 
97
- def _frame_path(employee_id: str):
98
- safe = employee_id.replace("/", "_")
99
  return os.path.join(LIVE_DB_ROOT, f"{safe}.jpg")
100
 
101
- def _compute_embedding(img, landmarks):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  try:
103
- return recognizer.get_normalized_embedding(img, landmarks)
 
 
104
  except Exception:
105
  return None
106
 
107
- # =========================
108
- # PIPELINE (FIXED)
109
- # =========================
110
  async def _run_pipeline(img_path: str, target_id: str):
111
  if detector is None or recognizer is None:
112
  return None
113
 
 
114
  img = cv2.imread(img_path)
115
  if img is None:
116
  return None
117
 
118
- # -------- STAGE 1: DETECT --------
 
119
  async with processing_lock:
120
  faces = await run_in_threadpool(detector.detect, img)
121
-
122
  if not faces:
123
  return None
124
 
125
- face = faces[0]
126
- bbox = list(map(float, face["bbox"]))
127
- landmarks = face["landmarks"]
128
- det_conf = float(face["confidence"])
129
-
130
- if det_conf < DETECTION_THRESHOLD:
 
 
 
 
 
 
131
  return {
132
- "bbox": bbox,
133
  "match_user": None,
134
  "confidence": 0.0,
135
  "authorized": False,
136
- "det_score": det_conf,
137
- "fake": True,
138
  "model_threshold": DETECTION_THRESHOLD,
139
  "raw_distance": 0.0,
140
- "reason": "low detection confidence",
141
  }
142
 
143
- # -------- STAGE 2: EMBEDDING --------
144
- probe_emb = await run_in_threadpool(_compute_embedding, img, landmarks)
 
145
  if probe_emb is None:
146
  return None
147
 
148
- best_score = 0.0
149
  best_user = None
150
 
151
- # users to search
152
  search_dirs = []
153
  if target_id == "*":
154
- search_dirs = [d for d in os.listdir(FACE_DB_ROOT) if not d.startswith("_")]
 
 
155
  else:
156
- if os.path.isdir(os.path.join(FACE_DB_ROOT, target_id)):
157
- search_dirs = [target_id]
158
 
159
- # -------- MATCHING --------
160
  for uid in search_dirs:
161
  user_dir = os.path.join(FACE_DB_ROOT, uid)
 
 
 
 
 
162
  for fname in os.listdir(user_dir):
163
- if not fname.lower().endswith((".jpg", ".png", ".jpeg")):
164
- continue
165
-
166
- ref_img = cv2.imread(os.path.join(user_dir, fname))
167
- if ref_img is None:
168
- continue
169
-
170
- # IMPORTANT FIX: lock + threadpool
171
- async with processing_lock:
172
- ref_faces = await run_in_threadpool(detector.detect, ref_img)
173
-
174
- if not ref_faces:
175
  continue
176
-
177
- ref_emb = _compute_embedding(ref_img, ref_faces[0]["landmarks"])
178
- if ref_emb is None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  continue
180
 
181
- sim = float(np.dot(probe_emb, ref_emb))
182
- if sim > best_score:
183
- best_score = sim
184
- best_user = uid
185
-
186
- authorized = best_user is not None and best_score >= SIM_THRESHOLD
187
-
188
  return {
189
- "bbox": bbox,
190
  "match_user": best_user if authorized else None,
191
- "confidence": round(best_score, 4),
192
  "authorized": authorized,
193
- "det_score": det_conf,
194
- "fake": False,
195
  "model_threshold": SIM_THRESHOLD,
196
- "raw_distance": round(best_score, 4),
197
  }
198
 
199
- # =========================
200
- # ENDPOINTS
201
- # =========================
202
  @app.get("/health")
203
  async def health():
204
- return {"status": "ok" if detector and recognizer else "model_error"}
 
 
 
205
 
206
- @app.post("/face/enroll")
207
- async def enroll(payload: EnrollRequest):
208
- path = os.path.join(FACE_DB_ROOT, payload.employee_id)
209
- os.makedirs(path, exist_ok=True)
210
 
 
 
 
 
 
 
 
211
  count = 0
212
  for s in payload.images:
213
  img = _b64_to_bgr(s)
214
  if img is None:
215
  continue
216
- cv2.imwrite(os.path.join(path, f"face_{count}.jpg"), img)
 
217
  count += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
- return {"employee_id": payload.employee_id, "added": count}
220
 
221
  @app.post("/face/verify")
222
- async def verify(
223
  request: Request,
224
- employee_id: str = Query("1"),
225
- threshold: float = Query(SIM_THRESHOLD),
226
  ):
 
 
227
  img = await _decode_image_from_request(request, ("frame", "image"))
228
  if img is None:
229
- raise HTTPException(400, "no image")
230
 
231
- frame_path = _frame_path(employee_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  cv2.imwrite(frame_path, img)
233
 
234
  det = await _run_pipeline(frame_path, employee_id)
235
  if det is None:
236
  return {
237
  "employee_id": employee_id,
238
- "authorized": False,
239
  "detections": [],
 
 
 
240
  }
241
-
242
  return {
243
  "employee_id": employee_id,
244
- "authorized": det["authorized"],
245
  "detections": [det],
 
 
246
  }
247
 
 
248
  @app.post("/face/clear")
249
- async def clear(payload: ClearRequest):
250
- path = os.path.join(FACE_DB_ROOT, payload.employee_id)
 
251
  removed = 0
252
- if os.path.isdir(path):
253
- for f in os.listdir(path):
254
- os.remove(os.path.join(path, f))
255
- removed += 1
256
- os.rmdir(path)
257
- return {"employee_id": payload.employee_id, "removed": removed}
 
 
 
 
 
 
 
2
  import base64
3
  import asyncio
4
  import json
5
+ from typing import List, Dict, Optional, Iterable
6
 
7
  import numpy as np
8
  import cv2
 
12
  from starlette.middleware.cors import CORSMiddleware
13
  from fastapi.responses import JSONResponse
14
 
15
+ # Import Uniface (Single Library)
16
  from uniface import RetinaFace, ArcFace
17
 
18
+ # Konfigurasi Threshold
19
+ # Stage 1: Detection Confidence (Pengganti Liveness Check sementara)
20
+ # Wajah dengan confidence di bawah ini dianggap tidak valid/buruk
21
+ DETECTION_THRESHOLD = 0.70
 
22
 
23
+ # Stage 2: Recognition Similarity (Cosine Similarity)
24
+ SIM_THRESHOLD = 0.40
25
+
26
+ app = FastAPI(
27
+ title="Face Verification API (Uniface Pure)",
28
+ version="2.2.0",
29
+ description="2-Stage Verification: High-Confidence Detection -> Uniface Recognition",
30
+ )
31
 
32
+ app.add_middleware(
33
+ CORSMiddleware,
34
+ allow_origins=["*"],
35
+ allow_credentials=True,
36
+ allow_methods=["*"],
37
+ allow_headers=["*"],
38
+ )
39
+
40
+ FACE_DB_ROOT = "face_db"
41
  os.makedirs(FACE_DB_ROOT, exist_ok=True)
42
+
43
+ LIVE_DB_ROOT = os.path.join(FACE_DB_ROOT, "_live")
44
  os.makedirs(LIVE_DB_ROOT, exist_ok=True)
45
 
46
  processing_lock = asyncio.Lock()
47
 
48
+ # --- INITIALIZATION (UNIFACE ONLY) ---
 
 
49
  detector = None
50
  recognizer = None
51
 
52
  try:
53
+ print("[Uniface] Initializing RetinaFace (Stage 1)...")
54
+ detector = RetinaFace()
55
+ print("[Uniface] Initializing ArcFace (Stage 2)...")
56
  recognizer = ArcFace()
57
  except Exception as e:
58
+ print(f"[Uniface] Error initializing models: {e}")
59
 
 
 
 
 
 
 
 
60
 
 
 
 
 
 
 
 
 
 
 
 
61
  class EnrollRequest(BaseModel):
62
+ employee_id: str = Field(default="1")
63
+ images: List[str] = Field(..., description="Base64 images")
64
 
65
  class VerifyImageRequest(BaseModel):
66
+ employee_id: str = Field(default="1")
67
+ image: str = Field(..., description="Base64 image")
68
+ threshold: Optional[float] = Field(default=None)
69
 
70
  class ClearRequest(BaseModel):
71
+ employee_id: str = Field(default="1")
72
+
73
 
 
 
 
74
  def _strip_b64(s: str) -> str:
75
+ if isinstance(s, str) and "," in s and s.lstrip().lower().startswith("data:"):
76
  return s.split(",", 1)[1]
77
  return s
78
 
79
+ def _b64_to_bgr(b64: str) -> Optional[np.ndarray]:
80
  try:
81
  raw = base64.b64decode(_strip_b64(b64), validate=False)
82
+ arr = np.frombuffer(raw, np.uint8)
83
+ return cv2.imdecode(arr, cv2.IMREAD_COLOR)
84
  except Exception:
85
  return None
86
 
87
+ def _bytes_to_bgr(data: bytes) -> Optional[np.ndarray]:
88
  try:
89
+ arr = np.frombuffer(data, np.uint8)
90
+ return cv2.imdecode(arr, cv2.IMREAD_COLOR)
91
  except Exception:
92
  return None
93
 
94
+ def _frame_path_for(employee_id: str) -> str:
95
+ safe = (employee_id or "live").replace("/", "_")
96
  return os.path.join(LIVE_DB_ROOT, f"{safe}.jpg")
97
 
98
+ async def _decode_image_from_request(request: Request, field_names: Iterable[str] = ("frame", "image")) -> Optional[np.ndarray]:
99
+ ct = (request.headers.get("content-type") or "").lower()
100
+ print(f"--- DEBUG START ---")
101
+ print(f"1. Content-Type yang diterima: {ct}")
102
+ if "multipart/form-data" in ct:
103
+ form = await request.form()
104
+ for name in field_names:
105
+ file = form.get(name)
106
+ if not file:
107
+ continue
108
+
109
+ if hasattr(file, "read"):
110
+ await file.seek(0)
111
+ data = await file.read()
112
+ img = _bytes_to_bgr(data)
113
+ if img is not None:
114
+ return img
115
+
116
+ elif isinstance(file, bytes):
117
+ img = _bytes_to_bgr(file)
118
+ if img is not None:
119
+ return img
120
+
121
+ # Coba decode
122
+ elif isinstance(file, str):
123
+ img = _b64_to_bgr(file)
124
+ if img is not None:
125
+ return img
126
+
127
+ return None
128
+ if "application/json" in ct or "text/json" in ct:
129
+ try:
130
+ obj = await request.json()
131
+ except Exception:
132
+ return None
133
+ for name in field_names:
134
+ val = obj.get(name)
135
+ if isinstance(val, str):
136
+ img = _b64_to_bgr(val)
137
+ if img is not None:
138
+ return img
139
+ return None
140
+ return None
141
+
142
+ def _compute_embedding_uniface(img_bgr, landmarks):
143
+ """Helper to get embedding using Uniface ArcFace"""
144
+ if recognizer is None:
145
+ return None
146
  try:
147
+ # get_normalized_embedding requires the image and landmarks
148
+ embedding = recognizer.get_normalized_embedding(img_bgr, landmarks)
149
+ return embedding
150
  except Exception:
151
  return None
152
 
 
 
 
153
  async def _run_pipeline(img_path: str, target_id: str):
154
  if detector is None or recognizer is None:
155
  return None
156
 
157
+ # Read image
158
  img = cv2.imread(img_path)
159
  if img is None:
160
  return None
161
 
162
+ # --- STAGE 1: DETECTION & QUALITY CHECK ---
163
+ # Menggunakan RetinaFace untuk mendeteksi wajah
164
  async with processing_lock:
165
  faces = await run_in_threadpool(detector.detect, img)
166
+
167
  if not faces:
168
  return None
169
 
170
+ # Ambil wajah dengan confidence tertinggi atau area terbesar
171
+ target_face = faces[0]
172
+ bbox = target_face['bbox'] # [x1, y1, x2, y2]
173
+ landmarks = target_face['landmarks']
174
+ confidence = target_face['confidence']
175
+
176
+ x1, y1, x2, y2 = map(int, bbox)
177
+
178
+ # Filter Stage 1: Check Confidence
179
+ # Jika confidence rendah, anggap sebagai "Fake" atau "Low Quality" dan tolak
180
+ if confidence < DETECTION_THRESHOLD:
181
+
182
  return {
183
+ "bbox": [float(x1), float(y1), float(x2), float(y2)],
184
  "match_user": None,
185
  "confidence": 0.0,
186
  "authorized": False,
187
+ "det_score": float(confidence),
188
+ "fake": True, # Flagged as fake/bad quality
189
  "model_threshold": DETECTION_THRESHOLD,
190
  "raw_distance": 0.0,
191
+ "reason": "Low quality/confidence detection"
192
  }
193
 
194
+ # --- STAGE 2: RECOGNITION (ArcFace) ---
195
+ # Jika lolos Stage 1, lanjut ke Recognition
196
+ probe_emb = await run_in_threadpool(_compute_embedding_uniface, img, landmarks)
197
  if probe_emb is None:
198
  return None
199
 
200
+ best_score = -1.0
201
  best_user = None
202
 
203
+ # Determine which users to check
204
  search_dirs = []
205
  if target_id == "*":
206
+ for d in os.listdir(FACE_DB_ROOT):
207
+ if not d.startswith("_"):
208
+ search_dirs.append(d)
209
  else:
210
+ if os.path.exists(os.path.join(FACE_DB_ROOT, target_id)):
211
+ search_dirs.append(target_id)
212
 
213
+ # Search in DB
214
  for uid in search_dirs:
215
  user_dir = os.path.join(FACE_DB_ROOT, uid)
216
+ files = os.listdir(user_dir)
217
+
218
+ if not files:
219
+ print(f"[DEBUG] Folder {uid} kosong")
220
+
221
  for fname in os.listdir(user_dir):
222
+ if not fname.lower().endswith(('.jpg', '.png', '.jpeg')):
 
 
 
 
 
 
 
 
 
 
 
223
  continue
224
+
225
+ ref_path = os.path.join(user_dir, fname)
226
+ try:
227
+ # Load ref image
228
+ # Note: In production, embeddings should be cached in memory/database
229
+ ref_img = cv2.imread(ref_path)
230
+ if ref_img is None: continue
231
+
232
+ ref_faces = detector.detect(ref_img)
233
+
234
+ if not ref_faces:
235
+ continue
236
+
237
+ # Get embedding
238
+ ref_emb = _compute_embedding_uniface(ref_img, ref_faces[0]['landmarks'])
239
+ if ref_emb is not None:
240
+ probe_flat = probe_emb.flatten()
241
+ ref_flat = ref_emb.flatten()
242
+ # Cosine similarity
243
+ sim = np.dot(probe_flat, ref_flat)
244
+ if sim > best_score:
245
+ best_score = sim
246
+ best_user = uid
247
+ except Exception:
248
  continue
249
 
250
+ authorized = bool(best_score > SIM_THRESHOLD)
251
+
 
 
 
 
 
252
  return {
253
+ "bbox": [float(x1), float(y1), float(x2), float(y2)],
254
  "match_user": best_user if authorized else None,
255
+ "confidence": round(float(best_score), 4),
256
  "authorized": authorized,
257
+ "det_score": float(confidence),
258
+ "fake": False, # Passed Stage 1
259
  "model_threshold": SIM_THRESHOLD,
260
+ "raw_distance": float(best_score)
261
  }
262
 
263
+
 
 
264
  @app.get("/health")
265
  async def health():
266
+ status = "ok"
267
+ if detector is None or recognizer is None:
268
+ status = "models_loading_or_failed"
269
+ return {"status": status, "system": "Uniface Pure (Stage1:Detect, Stage2:Recognize)"}
270
 
 
 
 
 
271
 
272
+ @app.post("/face/enroll")
273
+ async def face_enroll(payload: EnrollRequest):
274
+ if not payload.images:
275
+ raise HTTPException(status_code=400, detail="No images provided")
276
+ employee_id = payload.employee_id.strip() or "1"
277
+ save_dir = os.path.join(FACE_DB_ROOT, employee_id)
278
+ os.makedirs(save_dir, exist_ok=True)
279
  count = 0
280
  for s in payload.images:
281
  img = _b64_to_bgr(s)
282
  if img is None:
283
  continue
284
+ out_path = os.path.join(save_dir, f"face_{count}.jpg")
285
+ cv2.imwrite(out_path, img)
286
  count += 1
287
+ return {"employee_id": employee_id, "added": count, "total": len(os.listdir(save_dir))}
288
+
289
+
290
+ @app.post("/face/enroll-files")
291
+ async def face_enroll_files(
292
+ employee_id: str = Form(default="1"),
293
+ files: List[UploadFile] = File(default=[]),
294
+ ):
295
+ employee_id = employee_id.strip() or "1"
296
+ if not files:
297
+ raise HTTPException(status_code=400, detail="No files uploaded")
298
+ save_dir = os.path.join(FACE_DB_ROOT, employee_id)
299
+ os.makedirs(save_dir, exist_ok=True)
300
+ count = 0
301
+ for f in files:
302
+ try:
303
+ data = await f.read()
304
+ img = cv2.imdecode(np.frombuffer(data, np.uint8), cv2.IMREAD_COLOR)
305
+ if img is None:
306
+ continue
307
+ out_path = os.path.join(save_dir, f"face_{count}.jpg")
308
+ cv2.imwrite(out_path, img)
309
+ count += 1
310
+ except Exception:
311
+ continue
312
+ return {"employee_id": employee_id, "added": count, "total": len(os.listdir(save_dir))}
313
 
 
314
 
315
  @app.post("/face/verify")
316
+ async def face_verify(
317
  request: Request,
318
+ employee_id: str = Query(default="1"),
319
+ threshold: float = Query(default=SIM_THRESHOLD),
320
  ):
321
+ employee_id = employee_id.strip() or "1"
322
+
323
  img = await _decode_image_from_request(request, ("frame", "image"))
324
  if img is None:
325
+ raise HTTPException(status_code=400, detail="No image provided")
326
 
327
+ frame_path = _frame_path_for(employee_id)
328
+ cv2.imwrite(frame_path, img)
329
+
330
+ try:
331
+ det = await _run_pipeline(frame_path, employee_id)
332
+ print(f"[DEBUG] Pipeline result: {det}")
333
+ if det is None:
334
+ return {
335
+ "employee_id": employee_id,
336
+ "threshold": threshold,
337
+ "detections": [],
338
+ "count": 0,
339
+ "authorized": False,
340
+ "reason": "no face found",
341
+ }
342
+
343
+ return {
344
+ "employee_id": employee_id,
345
+ "threshold": threshold,
346
+ "detections": [det],
347
+ "count": 1,
348
+ "authorized": bool(det["authorized"]),
349
+ }
350
+ except Exception as e:
351
+ import traceback
352
+ traceback.print_exc()
353
+ return JSONResponse(status_code=500, content={"error": f"{type(e).__name__}: {e}"})
354
+
355
+
356
+ @app.websocket("/face/verify")
357
+ async def face_verify_ws(
358
+ websocket: WebSocket,
359
+ employee_id: str = Query(default="1"),
360
+ threshold: float = Query(default=SIM_THRESHOLD),
361
+ ):
362
+ await websocket.accept()
363
+ employee_id = employee_id.strip() or "1"
364
+
365
+ try:
366
+ while True:
367
+ try:
368
+ msg = await websocket.receive()
369
+ except WebSocketDisconnect:
370
+ break
371
+
372
+ img = None
373
+ if "bytes" in msg and msg["bytes"] is not None:
374
+ img = _bytes_to_bgr(msg["bytes"])
375
+ elif "text" in msg and msg["text"] is not None:
376
+ try:
377
+ obj = json.loads(msg["text"])
378
+ s = obj.get("frame") or obj.get("image")
379
+ if isinstance(s, str):
380
+ img = _b64_to_bgr(s)
381
+ except Exception:
382
+ img = None
383
+
384
+ if img is None:
385
+ try:
386
+ await websocket.send_json({"error": "no/invalid frame"})
387
+ except (RuntimeError, WebSocketDisconnect):
388
+ break
389
+ continue
390
+
391
+ frame_path = _frame_path_for(employee_id)
392
+ cv2.imwrite(frame_path, img)
393
+
394
+ try:
395
+ det = await _run_pipeline(frame_path, employee_id)
396
+ except Exception as e:
397
+ try:
398
+ await websocket.send_json({"error": f"{type(e).__name__}: {e}"})
399
+ except (RuntimeError, WebSocketDisconnect):
400
+ break
401
+ continue
402
+
403
+ if det is None:
404
+ payload = {
405
+ "employee_id": employee_id,
406
+ "threshold": threshold,
407
+ "detections": [],
408
+ "count": 0,
409
+ "authorized": False,
410
+ "reason": "no face found",
411
+ }
412
+ else:
413
+ payload = {
414
+ "employee_id": employee_id,
415
+ "threshold": threshold,
416
+ "detections": [det],
417
+ "count": 1,
418
+ "authorized": bool(det["authorized"]),
419
+ }
420
+
421
+ try:
422
+ await websocket.send_json(payload)
423
+ except (RuntimeError, WebSocketDisconnect):
424
+ break
425
+ finally:
426
+ return
427
+
428
+
429
+ @app.post("/face/verify-image")
430
+ async def face_verify_image(payload: VerifyImageRequest):
431
+ img = _b64_to_bgr(payload.image)
432
+ if img is None:
433
+ raise HTTPException(status_code=400, detail="Invalid or missing image")
434
+ employee_id = payload.employee_id.strip() or "1"
435
+ thr = payload.threshold if payload.threshold is not None else SIM_THRESHOLD
436
+
437
+ frame_path = _frame_path_for(employee_id)
438
  cv2.imwrite(frame_path, img)
439
 
440
  det = await _run_pipeline(frame_path, employee_id)
441
  if det is None:
442
  return {
443
  "employee_id": employee_id,
444
+ "threshold": thr,
445
  "detections": [],
446
+ "count": 0,
447
+ "authorized": False,
448
+ "reason": "no face found",
449
  }
 
450
  return {
451
  "employee_id": employee_id,
452
+ "threshold": thr,
453
  "detections": [det],
454
+ "count": 1,
455
+ "authorized": bool(det["authorized"]),
456
  }
457
 
458
+
459
  @app.post("/face/clear")
460
+ async def face_clear(payload: ClearRequest):
461
+ employee_id = payload.employee_id.strip() or "1"
462
+ folder = os.path.join(FACE_DB_ROOT, employee_id)
463
  removed = 0
464
+ if os.path.isdir(folder):
465
+ for f in os.listdir(folder):
466
+ try:
467
+ os.remove(os.path.join(folder, f))
468
+ removed += 1
469
+ except Exception:
470
+ pass
471
+ try:
472
+ os.rmdir(folder)
473
+ except Exception:
474
+ pass
475
+ return {"employee_id": employee_id, "removed": removed}