HSB3119-22080292-daothivananh commited on
Commit
fa184e8
·
1 Parent(s): 43de8f8

ygfdbvghjfd

Browse files
Files changed (1) hide show
  1. controllers/main.py +108 -520
controllers/main.py CHANGED
@@ -1,460 +1,18 @@
1
- # """
2
- # main.py — FastAPI + InsightFace + In-Memory RAM + File Image Storage
3
- # ─────────────────────────────────────────────────────────────────────
4
- # Thay đổi so với phiên bản cũ:
5
- # • Dùng InsightFace thay face_recognition → nhanh ~15-20×
6
- # • Ảnh lưu trên ổ đĩa (/uploads/) thay vì Base64 trong DB
7
- # • Endpoint GET /uploads/{filename} để frontend hiển thị ảnh
8
- # • match["distance"] là cosine distance (InsightFace), không phải Euclidean
9
- # """
10
-
11
- # import uuid
12
- # import json
13
- # import time
14
- # import logging
15
- # from contextlib import asynccontextmanager
16
- # from pathlib import Path
17
-
18
- # from fastapi import FastAPI, UploadFile, File, Form, BackgroundTasks
19
- # from fastapi.middleware.cors import CORSMiddleware
20
- # from fastapi.responses import JSONResponse, FileResponse
21
- # from fastapi.staticfiles import StaticFiles
22
- # from pydantic import BaseModel
23
-
24
- # from database.database import get_db_connection
25
- # from service.face_service import face_ai_service, face_memory_store, UPLOAD_DIR
26
-
27
- # logging.basicConfig(level=logging.INFO)
28
- # logger = logging.getLogger(__name__)
29
-
30
-
31
- # # ─── Startup ──────────────────────────────────────────────────────────────────
32
- # @asynccontextmanager
33
- # async def lifespan(app: FastAPI):
34
- # logger.info("[Startup] 🚀 Đang nạp embedding vào RAM...")
35
- # _load_embeddings_to_ram()
36
- # logger.info(f"[Startup] ✅ Sẵn sàng — {face_memory_store.count} khuôn mặt trên RAM")
37
- # yield
38
- # logger.info("[Shutdown] Bye!")
39
-
40
-
41
- # def _load_embeddings_to_ram() -> None:
42
- # try:
43
- # conn = get_db_connection()
44
- # cursor = conn.cursor(dictionary=True)
45
- # cursor.execute("""
46
- # SELECT e.person_id, p.name, p.role, p.img_path, e.embedding_vector
47
- # FROM face_embeddings e
48
- # JOIN persons p ON e.person_id = p.id
49
- # WHERE p.status = 'active'
50
- # """)
51
- # rows = cursor.fetchall()
52
- # cursor.close()
53
- # conn.close()
54
-
55
- # parsed = []
56
- # for row in rows:
57
- # try:
58
- # parsed.append({
59
- # "person_id": row["person_id"],
60
- # "name": row["name"],
61
- # "role": row.get("role", ""),
62
- # "img_path": row.get("img_path", ""),
63
- # "embedding_vector": json.loads(row["embedding_vector"]),
64
- # })
65
- # except Exception as e:
66
- # logger.warning(f"[Startup] Bỏ qua: {e}")
67
-
68
- # face_memory_store.load_all(parsed)
69
- # except Exception as e:
70
- # logger.error(f"[Startup] ❌ {e}")
71
-
72
-
73
- # # ─── App ──────────────────────────────────────────────────────────────────────
74
- # app = FastAPI(lifespan=lifespan)
75
-
76
- # app.add_middleware(
77
- # CORSMiddleware,
78
- # allow_origins=["*"],
79
- # allow_credentials=True,
80
- # allow_methods=["*"],
81
- # allow_headers=["*"],
82
- # )
83
-
84
- # # Serve ảnh tĩnh: GET /uploads/abc.jpg
85
- # app.mount("/uploads", StaticFiles(directory=UPLOAD_DIR), name="uploads")
86
-
87
-
88
- # # ─── Models ───────────────────────────────────────────────────────────────────
89
- # class PersonUpdate(BaseModel):
90
- # name: str
91
- # role: str
92
- # department: str
93
-
94
-
95
- # # ─── Background: lưu log ──────────────────────────────────────────────────────
96
- # def save_log_to_db(log_queries: list) -> None:
97
- # if not log_queries:
98
- # return
99
- # try:
100
- # conn = get_db_connection()
101
- # cursor = conn.cursor()
102
- # cursor.executemany(
103
- # "INSERT INTO recognition_logs (id, person_id, status, confidence, camera, action) VALUES (%s,%s,%s,%s,%s,%s)",
104
- # log_queries,
105
- # )
106
- # conn.commit()
107
- # cursor.close()
108
- # conn.close()
109
- # except Exception as e:
110
- # logger.error(f"[Log] {e}")
111
-
112
-
113
- # # ═════════════════════════════════════════════════════════════════════════════
114
- # # NHẬN DIỆN — chỉ RAM, không DB
115
- # # ═══════════════════════════════════════════════════════════════════════���═════
116
- # @app.post("/api/face/recognize")
117
- # async def recognize(
118
- # background_tasks: BackgroundTasks,
119
- # image: UploadFile = File(...),
120
- # ):
121
- # t0 = time.time()
122
- # file_bytes = await image.read()
123
-
124
- # detections = face_ai_service.extract_faces(file_bytes)
125
- # if not detections:
126
- # return {"success": True, "data": {"detected": False, "faces": []}}
127
-
128
- # results = []
129
- # log_queries = []
130
-
131
- # for face in detections:
132
- # import numpy as np
133
- # query_enc = np.array(face["descriptor"], dtype=np.float32)
134
- # bbox = face["box"]
135
- # det_score = face.get("det_score", 1.0)
136
-
137
- # # So khớp trên RAM
138
- # match = face_memory_store.find_best_match(query_enc)
139
-
140
- # if match:
141
- # # Chuyển cosine distance → confidence %
142
- # confidence = round(max(0.0, (1.0 - match["distance"]) * 100.0), 2)
143
- # logger.info(
144
- # f"[Recognize] ✅ {match['name']} | "
145
- # f"dist={match['distance']:.4f} | conf={confidence:.1f}%"
146
- # )
147
-
148
- # # URL ảnh để frontend hiển thị
149
- # img_url = (
150
- # f"/uploads/{Path(match['img_path']).name}"
151
- # if match.get("img_path") else ""
152
- # )
153
-
154
- # results.append({
155
- # "id": match["person_id"],
156
- # "name": match["name"],
157
- # "role": match["role"],
158
- # "img": img_url,
159
- # "status": "success",
160
- # "confidence": confidence,
161
- # "bbox": bbox,
162
- # "det_score": det_score,
163
- # })
164
- # log_queries.append((
165
- # str(uuid.uuid4()), match["person_id"],
166
- # "success", confidence, "Cổng Chính", "Vào",
167
- # ))
168
- # else:
169
- # results.append({
170
- # "id": None,
171
- # "name": "Người Lạ",
172
- # "role": "",
173
- # "img": "",
174
- # "status": "unknown",
175
- # "confidence": 0,
176
- # "bbox": bbox,
177
- # })
178
- # log_queries.append((
179
- # str(uuid.uuid4()), None,
180
- # "unknown", 0, "Cổng Chính", "Từ chối",
181
- # ))
182
-
183
- # background_tasks.add_task(save_log_to_db, log_queries)
184
-
185
- # return {
186
- # "success": True,
187
- # "data": {
188
- # "detected": True,
189
- # "faces": results,
190
- # "processTime": int((time.time() - t0) * 1000),
191
- # "model": "InsightFace-buffalo_sc-RAM",
192
- # "ramCount": face_memory_store.count,
193
- # },
194
- # }
195
-
196
-
197
- # # ═════════════════════════════════════════════════════════════════════════════
198
- # # ĐĂNG KÝ — lưu ảnh file + ghi DB + cập nhật RAM
199
- # # ═════════════════════════════════════════════════════════════════════════════
200
- # @app.post("/api/face/register")
201
- # async def register(
202
- # name: str = Form(...),
203
- # role: str = Form(""),
204
- # department: str = Form(""),
205
- # images: list[UploadFile] = File(...),
206
- # ):
207
- # conn = get_db_connection()
208
- # cursor = conn.cursor()
209
-
210
- # person_id = str(uuid.uuid4())
211
- # new_encodings: list[tuple] = []
212
- # avatar_path = ""
213
-
214
- # try:
215
- # for i, img_file in enumerate(images):
216
- # img_bytes = await img_file.read()
217
-
218
- # # Lưu ảnh lên disk
219
- # saved_path = face_ai_service.save_image(img_bytes, person_id, index=i)
220
- # if i == 0:
221
- # avatar_path = saved_path # ảnh đầu làm avatar
222
-
223
- # # Trích xuất embedding
224
- # detections = face_ai_service.extract_faces(img_bytes)
225
-
226
- # if len(detections) == 0:
227
- # raise Exception(f"Không tìm thấy khuôn mặt trong ảnh thứ {i + 1}.")
228
- # if len(detections) > 1:
229
- # raise Exception(f"Ảnh thứ {i + 1} có nhiều hơn 1 khuôn mặt.")
230
-
231
- # descriptor = detections[0]["descriptor"]
232
- # embedding_json = json.dumps(descriptor)
233
- # embedding_id = str(uuid.uuid4())
234
-
235
- # if i == 0:
236
- # # Insert person record (dùng img_path thay vì Base64)
237
- # cursor.execute(
238
- # """INSERT INTO persons (id, name, role, department, status, img_path)
239
- # VALUES (%s, %s, %s, %s, 'active', %s)""",
240
- # (person_id, name, role, department, avatar_path),
241
- # )
242
-
243
- # cursor.execute(
244
- # "INSERT INTO face_embeddings (id, person_id, embedding_vector) VALUES (%s, %s, %s)",
245
- # (embedding_id, person_id, embedding_json),
246
- # )
247
- # new_encodings.append((person_id, name, role, avatar_path, descriptor))
248
-
249
- # conn.commit()
250
-
251
- # # ✅ Cập nhật RAM ngay — nhận diện có hiệu lực lập tức
252
- # for pid, pname, prole, pimg, enc in new_encodings:
253
- # face_memory_store.add(pid, pname, prole, pimg, enc)
254
-
255
- # logger.info(f"[Register] ✅ {name} | {len(new_encodings)} mẫu | RAM: {face_memory_store.count}")
256
-
257
- # return {
258
- # "success": True,
259
- # "message": f"Đã đăng ký {name} với {len(new_encodings)} mẫu.",
260
- # "img_url": f"/uploads/{Path(avatar_path).name}",
261
- # "ramCount": face_memory_store.count,
262
- # }
263
-
264
- # except Exception as e:
265
- # conn.rollback()
266
- # logger.error(f"[Register] ❌ {e}")
267
- # # Xóa ảnh đã lưu nếu lỗi
268
- # for i in range(len(images)):
269
- # p = Path(UPLOAD_DIR) / f"{person_id}_{i}.jpg"
270
- # if p.exists():
271
- # p.unlink()
272
- # return JSONResponse(status_code=400, content={"success": False, "error": str(e)})
273
- # finally:
274
- # cursor.close()
275
- # conn.close()
276
-
277
-
278
- # # ═════════════════════════════════════════════════════════════════════════════
279
- # # DANH SÁCH NGƯỜI DÙNG
280
- # # ═════════════════════════════════════════════════════════════════════════════
281
- # @app.get("/api/face/persons")
282
- # async def get_persons():
283
- # conn = get_db_connection()
284
- # cursor = conn.cursor(dictionary=True)
285
- # try:
286
- # cursor.execute("""
287
- # SELECT p.id, p.name, p.role, p.department, p.status,
288
- # p.img_path, p.registered_at AS registered,
289
- # (SELECT COUNT(*) FROM face_embeddings e WHERE e.person_id = p.id) AS embeddings,
290
- # (SELECT COUNT(*) FROM recognition_logs l WHERE l.person_id = p.id AND l.status='success') AS recognitions
291
- # FROM persons p ORDER BY p.registered_at DESC
292
- # """)
293
- # rows = cursor.fetchall()
294
-
295
- # # Chuyển img_path → img_url
296
- # for row in rows:
297
- # raw = row.get("img_path") or ""
298
- # row["img"] = f"/uploads/{Path(raw).name}" if raw else ""
299
-
300
- # return {"success": True, "data": rows, "total": len(rows), "ramCount": face_memory_store.count}
301
- # finally:
302
- # cursor.close()
303
- # conn.close()
304
-
305
-
306
- # # ═════════════════════════════════════════════════════════════════════════════
307
- # # CẬP NHẬT
308
- # # ═════════════════════════════════════════════════════════════════════════════
309
- # @app.put("/api/face/persons/{id}")
310
- # async def update_person(id: str, person_data: PersonUpdate):
311
- # conn = get_db_connection()
312
- # cursor = conn.cursor()
313
- # try:
314
- # cursor.execute(
315
- # "UPDATE persons SET name=%s, role=%s, department=%s WHERE id=%s",
316
- # (person_data.name, person_data.role, person_data.department, id),
317
- # )
318
- # conn.commit()
319
- # if cursor.rowcount == 0:
320
- # return JSONResponse(status_code=404, content={"success": False, "error": "Không tìm thấy"})
321
-
322
- # face_memory_store.update_info(id, person_data.name, person_data.role)
323
- # return {"success": True, "message": "Cập nhật thành công"}
324
- # finally:
325
- # cursor.close()
326
- # conn.close()
327
-
328
-
329
- # # ═════════════════════════════════════════════════════════════════════════════
330
- # # XÓA
331
- # # ═════════════════════════════════════════════════════════════════════════════
332
- # @app.delete("/api/face/persons/{id}")
333
- # async def delete_person(id: str):
334
- # conn = get_db_connection()
335
- # cursor = conn.cursor(dictionary=True)
336
- # try:
337
- # # Lấy img_path để xóa file
338
- # cursor.execute("SELECT img_path FROM persons WHERE id=%s", (id,))
339
- # row = cursor.fetchone()
340
-
341
- # cursor2 = conn.cursor()
342
- # cursor2.execute("DELETE FROM persons WHERE id=%s", (id,))
343
- # conn.commit()
344
-
345
- # if cursor2.rowcount == 0:
346
- # return JSONResponse(status_code=404, content={"success": False, "error": "Không tìm thấy"})
347
-
348
- # # Xóa file ảnh trên disk
349
- # if row and row.get("img_path"):
350
- # p = Path(row["img_path"])
351
- # if p.exists():
352
- # p.unlink()
353
-
354
- # removed = face_memory_store.remove_by_person(id)
355
- # return {"success": True, "message": "Đã xóa", "removedFromRam": removed}
356
- # finally:
357
- # cursor.close()
358
- # conn.close()
359
-
360
-
361
- # # ═════════════════════════════════════════════════════════════════════════════
362
- # # LỊCH SỬ
363
- # # ═════════════════════════════════════════════════════════════════════════════
364
- # @app.get("/api/face/logs")
365
- # async def get_logs():
366
- # conn = get_db_connection()
367
- # cursor = conn.cursor(dictionary=True)
368
- # try:
369
- # cursor.execute("""
370
- # SELECT l.id, COALESCE(p.name,'Người lạ') AS name,
371
- # DATE_FORMAT(l.created_at,'%H:%i:%s') AS time,
372
- # DATE_FORMAT(l.created_at,'%d/%m/%Y') AS date,
373
- # l.status, l.confidence, l.camera, l.action,
374
- # p.img_path AS img_raw
375
- # FROM recognition_logs l
376
- # LEFT JOIN persons p ON l.person_id = p.id
377
- # ORDER BY l.created_at DESC LIMIT 100
378
- # """)
379
- # rows = cursor.fetchall()
380
- # for row in rows:
381
- # raw = row.pop("img_raw", "") or ""
382
- # row["img"] = f"/uploads/{Path(raw).name}" if raw else ""
383
- # return {"success": True, "data": rows, "total": len(rows)}
384
- # finally:
385
- # cursor.close()
386
- # conn.close()
387
-
388
-
389
- # # ═════════════════════════════════════════════════════════════════════════════
390
- # # THỐNG KÊ
391
- # # ═════════════════════════════════════════════════════════════════════════════
392
- # @app.get("/api/face/statistics")
393
- # async def get_statistics():
394
- # conn = get_db_connection()
395
- # cursor = conn.cursor(dictionary=True)
396
- # try:
397
- # cursor.execute("SELECT status, created_at FROM recognition_logs ORDER BY created_at DESC LIMIT 1000")
398
- # all_logs = cursor.fetchall()
399
-
400
- # hourly = {f"{i:02d}:00": {"nhận_diện": 0, "từ_chối": 0, "lạ": 0} for i in range(24)}
401
- # days = ["T2","T3","T4","T5","T6","T7","CN"]
402
- # weekly = {d: 0 for d in days}
403
-
404
- # for log in all_logs:
405
- # h = f"{log['created_at'].hour:02d}:00"
406
- # d = days[log["created_at"].weekday()]
407
- # if log["status"] == "success":
408
- # hourly[h]["nhận_diện"] += 1
409
- # weekly[d] += 1
410
- # elif log["status"] == "unknown":
411
- # hourly[h]["lạ"] += 1
412
-
413
- # return {
414
- # "success": True,
415
- # "data": {
416
- # "hourlyData": [{"time": t, **v} for t, v in hourly.items()],
417
- # "weeklyData": [{"day": d, "value": v} for d, v in weekly.items()],
418
- # },
419
- # }
420
- # finally:
421
- # cursor.close()
422
- # conn.close()
423
-
424
-
425
- # # ═════════════════════════════════════════════════════════════════════════════
426
- # # DEBUG
427
- # # ═════════════════════════════════════════════════════════════════════════════
428
- # @app.get("/api/face/memory-status")
429
- # async def memory_status():
430
- # return {
431
- # "success": True,
432
- # "loaded": face_memory_store.is_loaded,
433
- # "ramCount": face_memory_store.count,
434
- # }
435
-
436
- # @app.post("/api/face/reload-memory")
437
- # async def reload_memory():
438
- # _load_embeddings_to_ram()
439
- # return {"success": True, "ramCount": face_memory_store.count}
440
-
441
-
442
- # if __name__ == "__main__":
443
- # import uvicorn
444
- # uvicorn.run(app, host="0.0.0.0", port=3001)
445
-
446
-
447
  import os
448
  import sys
449
  from pathlib import Path
 
 
 
 
 
 
 
450
 
451
  # ─── 1. CẤU HÌNH ĐƯỜNG DẪN TUYỆT ĐỐI (TRÁNH LẠC ĐƯỜNG) ────���─────────────────
452
- # Lấy đường dẫn của thư mục 'controllers' hiện tại
453
  current_dir = os.path.dirname(os.path.abspath(__file__))
454
- # Lấy đường dẫn của thư mục gốc 'server'
455
  root_dir = os.path.dirname(current_dir)
456
 
457
- # Đưa các thư mục vào tầm ngắm của Python
458
  sys.path.insert(0, current_dir)
459
  sys.path.insert(0, os.path.join(current_dir, 'DetecInfoBoxes'))
460
  if root_dir not in sys.path:
@@ -471,7 +29,7 @@ from contextlib import asynccontextmanager
471
  from datetime import date
472
  from dotenv import load_dotenv
473
 
474
- # ─── 3. IMPORT CHUẨN (KHÔNG CÒN LỖI MODULE) ───────────────────────────────────
475
  from readInfoIdCard import ReadInfo
476
  from DetecInfoBoxes.GetBoxes import Detect
477
  from Vocr.tool.predictor import Predictor
@@ -493,23 +51,33 @@ from service.face_service import face_ai_service, face_memory_store, UPLOAD_DIR
493
  logging.basicConfig(level=logging.INFO)
494
  logger = logging.getLogger(__name__)
495
 
 
 
496
 
497
- # ─── KHỞI TẠO AI: YOLOv7 + VietOCR ────────────────────────────────────────────
498
- logger.info("[Startup] Nạp mô hình VietOCR (VGG-seq2seq)...")
499
- vocr_config_path = os.path.join(current_dir, 'Vocr', 'config', 'vgg-seq2seq.yml')
500
- config_vietocr = Cfg_vietocr.load_config_from_file(vocr_config_path)
501
 
502
- config_vietocr['weights'] = os.path.join(current_dir, 'Models', 'seq2seqocr.pth')
503
- config_vietocr['device'] = 'cpu' # Đổi thành 'cuda:0' nếu máy có Card rời NVIDIA
504
- ocr_predictor = Predictor(config_vietocr)
505
-
506
- logger.info("[Startup] Nạp mô hình YOLOv7 (Phát hiện vùng thông tin)...")
507
- get_dictionary = Detect(opt)
508
- scan_weight = os.path.join(current_dir, 'Models', 'cccdYoloV7.pt')
509
- imgsz, stride, device, half, model, names = get_dictionary.load_model(scan_weight)
510
-
511
- read_info = ReadInfo(imgsz, stride, device, half, model, names, ocr_predictor)
512
- logger.info("[Startup] Hệ thống YOLO + VietOCR đã sẵn sàng!")
 
 
 
 
 
 
 
 
513
 
514
 
515
  # ─── Startup ──────────────────────────────────────────────────────────────────
@@ -520,8 +88,12 @@ async def lifespan(app: FastAPI):
520
  logger.info("[Startup] Nạp embedding vào RAM...")
521
  _load_embeddings_to_ram()
522
  logger.info(f"[Startup] {face_memory_store.count} khuôn mặt trên RAM")
 
 
 
523
  yield
524
  logger.info("[Shutdown] Bye!")
 
525
  def _load_embeddings_to_ram():
526
  conn = None
527
  cursor = None
@@ -529,7 +101,7 @@ def _load_embeddings_to_ram():
529
  conn = get_db_connection()
530
  cursor = conn.cursor(dictionary=True)
531
  cursor.execute("""
532
- SELECT e.person_id, p.name, p.role, p.img_path,
533
  p.work_expiry_date, e.embedding_vector
534
  FROM face_embeddings e
535
  JOIN persons p ON e.person_id = p.id
@@ -539,20 +111,22 @@ def _load_embeddings_to_ram():
539
  parsed = []
540
  for row in rows:
541
  try:
 
 
542
  parsed.append({
543
  "person_id": row["person_id"],
544
  "name": row["name"],
545
  "role": row.get("role", ""),
546
- "img_path": row.get("img_path", ""),
547
  "work_expiry_date": str(row["work_expiry_date"]) if row.get("work_expiry_date") else None,
548
  "embedding_vector": json.loads(row["embedding_vector"]),
549
  })
550
  except Exception as e:
551
  logger.warning(f"[Startup] Bỏ qua khuôn mặt lỗi: {e}")
552
- face_memory_store.load_all(parsed)
553
 
554
  except Exception as e:
555
- logger.error(f"[Startup] Lỗi kết nối DB khi nạp dữ liệu: {e}")
556
  face_memory_store.load_all([])
557
 
558
  finally:
@@ -573,13 +147,11 @@ app.add_middleware(
573
 
574
  app.mount("/uploads", StaticFiles(directory=UPLOAD_DIR), name="uploads")
575
 
576
-
577
  class PersonUpdate(BaseModel):
578
  name: str
579
  role: str
580
  department: str
581
 
582
-
583
  def save_log_to_db(log_queries: list) -> None:
584
  if not log_queries:
585
  return
@@ -597,8 +169,15 @@ def save_log_to_db(log_queries: list) -> None:
597
  logger.error(f"[Log] {e}")
598
 
599
 
 
 
 
600
  @app.post("/api/face/ocr")
601
  async def extract_ocr_local(file: UploadFile = File(...), side: str = Form(...)):
 
 
 
 
602
  temp_path = ""
603
  try:
604
  temp_filename = f"temp_cccd_{uuid.uuid4().hex}.jpg"
@@ -607,11 +186,10 @@ async def extract_ocr_local(file: UploadFile = File(...), side: str = Form(...))
607
  with open(temp_path, "wb") as f:
608
  f.write(file_bytes)
609
 
610
- logger.info(f"[OCR] Phân tích mặt {side} bằng YOLOv7 + VietOCR...")
611
 
612
  if side == "front":
613
  raw = read_info.get_all_info(temp_path)
614
- logger.info(f"[OCR] Mặt trước raw: {raw}")
615
  mapped_data = {
616
  "id_number": raw.get("id", ""),
617
  "full_name": raw.get("full_name", ""),
@@ -622,10 +200,8 @@ async def extract_ocr_local(file: UploadFile = File(...), side: str = Form(...))
622
  "address": raw.get("place_of_residence", ""),
623
  "expiry_date": raw.get("date_of_expiry", ""),
624
  }
625
-
626
- else: # back — ĐÃ NÂNG CẤP: dùng get_back_info như mặt trước
627
  raw = read_info.get_back_info(temp_path)
628
- logger.info(f"[OCR] Mặt sau raw: {raw}")
629
  mapped_data = {
630
  "issue_date": raw.get("issue_date", ""),
631
  "issued_by": raw.get("issued_by", ""),
@@ -635,18 +211,17 @@ async def extract_ocr_local(file: UploadFile = File(...), side: str = Form(...))
635
  if os.path.exists(temp_path):
636
  os.remove(temp_path)
637
 
638
- logger.info(f"[OCR] Trả về React: {mapped_data}")
639
  return {"success": True, "data": mapped_data}
640
 
641
  except Exception as e:
642
- logger.error(f"[OCR] Lỗi: {e}", exc_info=True)
643
  if os.path.exists(temp_path):
644
  os.remove(temp_path)
645
  return {"success": True, "data": {}}
646
 
647
 
648
  # ═════════════════════════════════════════════════════════════════════════════
649
- # NHẬN DIỆN KHUÔN MẶT
650
  # ═════════════════════════════════════════════════════════════════════════════
651
  @app.post("/api/face/recognize")
652
  async def recognize(
@@ -668,11 +243,9 @@ async def recognize(
668
  match = face_memory_store.find_best_match(np.array(face["descriptor"], dtype=np.float32))
669
 
670
  if match:
671
- # ── Kiểm tra hết hạn làm việc ────────────────────────────
672
  expiry_str = match.get("work_expiry_date")
673
  if expiry_str:
674
  if date.fromisoformat(expiry_str) < today:
675
- logger.info(f"[Recognize] {match['name']} — HẾT HẠN {expiry_str}")
676
  results.append({
677
  "id": match["person_id"], "name": match["name"],
678
  "role": match["role"], "img": "",
@@ -683,8 +256,12 @@ async def recognize(
683
  continue
684
 
685
  confidence = round(max(0.0, (1.0 - match["distance"]) * 100.0), 2)
686
- img_url = f"/uploads/{Path(match['img_path']).name}" if match.get("img_path") else ""
687
- logger.info(f"[Recognize] {match['name']} dist={match['distance']:.4f} conf={confidence:.1f}%")
 
 
 
 
688
  results.append({
689
  "id": match["person_id"], "name": match["name"],
690
  "role": match["role"], "img": img_url,
@@ -713,15 +290,15 @@ async def recognize(
713
 
714
 
715
  # ═════════════════════════════════════════════════════════════════════════════
716
- # ĐĂNG KÝ
717
  # ═════════════════════════════════════════════════════════════════════════════
718
  @app.post("/api/face/register")
719
  async def register(
720
  name: str = Form(...),
721
  role: str = Form(""),
722
  department: str = Form(""),
723
- work_expiry_date: str = Form(""), # YYYY-MM-DD hoặc ""
724
- cccd_info: str = Form("{}"), # JSON string từ StepCCCD
725
  images: list[UploadFile] = File(...),
726
  cccd_front: UploadFile = File(None),
727
  cccd_back: UploadFile = File(None),
@@ -730,10 +307,12 @@ async def register(
730
  cursor = conn.cursor()
731
  person_id = str(uuid.uuid4())
732
  new_encodings: list[tuple] = []
 
 
 
733
  avatar_path = ""
734
- saved_files = [] # Lưu danh sách file đã tạo để xóa nếu có lỗi
735
 
736
- # ĐỊNH NGHĨA NGƯỠNG SO SÁNH KHUÔN MẶT (Càng cao càng khắt khe, khuyên dùng 0.35 - 0.45)
737
  COSINE_THRESHOLD = 0.4
738
 
739
  try:
@@ -741,13 +320,13 @@ async def register(
741
  expiry_val = work_expiry_date or None
742
  cccd_number = cccd.get("id_number")
743
 
744
- # ── 1. CHECK TRÙNG CCCD ─────────────────────────────────────────────
745
  if cccd_number:
746
  cursor.execute("SELECT id FROM citizen_ids WHERE id_number = %s", (cccd_number,))
747
  if cursor.fetchone():
748
  raise Exception("Số CCCD này đã được đăng ký trong hệ thống!")
749
 
750
- # ── 2. XỬ ẢNH KHUÔN MẶT WEBCAM/UPLOAD ────────────────────────────
751
  user_descriptor = None
752
  for i, img_file in enumerate(images):
753
  img_bytes = await img_file.read()
@@ -762,41 +341,42 @@ async def register(
762
  emb_id = str(uuid.uuid4())
763
 
764
  if i == 0:
765
- user_descriptor = descriptor # Lưu ảnh đầu tiên để so sánh với CCCD
766
 
767
- # Lưu file cứng
768
- saved_path = face_ai_service.save_image(img_bytes, person_id, index=i)
769
  saved_files.append(saved_path)
770
 
771
  if i == 0:
772
  avatar_path = saved_path
 
773
  cursor.execute(
774
  """INSERT INTO persons
775
- (id, name, role, department, status, img_path, work_expiry_date)
776
- VALUES (%s, %s, %s, %s, 'active', %s, %s)""",
777
- (person_id, name, role, department, avatar_path, expiry_val),
778
  )
779
 
780
  cursor.execute(
781
  "INSERT INTO face_embeddings (id, person_id, embedding_vector) VALUES (%s, %s, %s)",
782
  (emb_id, person_id, json.dumps(descriptor)),
783
  )
784
- new_encodings.append((person_id, name, role, avatar_path, expiry_val, descriptor))
 
 
 
785
 
786
- # ── 3. XỬ CCCD VÀ SO SÁNH KHUÔN MẶT ──────────────────────────────
787
  front_path, back_path = "", ""
788
 
789
  if cccd_front:
790
  fb_bytes = await cccd_front.read()
791
  if fb_bytes:
792
- # Trích xuất khuôn mặt từ ảnh mặt trước CCCD
793
  cccd_detections = face_ai_service.extract_faces(fb_bytes)
794
  if len(cccd_detections) == 0:
795
  raise Exception("Không tìm thấy khuôn mặt trên ảnh mặt trước CCCD.")
796
 
797
  cccd_descriptor = cccd_detections[0]["descriptor"]
798
-
799
- # So sánh độ tương đồng (Cosine Similarity)
800
  q = face_memory_store._norm(np.array(user_descriptor, dtype=np.float32))
801
  c = face_memory_store._norm(np.array(cccd_descriptor, dtype=np.float32))
802
  score = float(np.dot(q, c))
@@ -805,13 +385,15 @@ async def register(
805
  logger.warning(f"Cảnh báo giả mạo: Score {score} < {COSINE_THRESHOLD}")
806
  raise Exception("Cảnh báo: Khuôn mặt trên thẻ CCCD KHÔNG KHỚP với ảnh chụp trực tiếp!")
807
 
808
- front_path = face_ai_service.save_image(fb_bytes, f"cccd_front_{person_id}", index=0)
 
809
  saved_files.append(front_path)
810
 
811
  if cccd_back:
812
  bb_bytes = await cccd_back.read()
813
  if bb_bytes:
814
- back_path = face_ai_service.save_image(bb_bytes, f"cccd_back_{person_id}", index=0)
 
815
  saved_files.append(back_path)
816
 
817
  cursor.execute("""
@@ -833,22 +415,20 @@ async def register(
833
 
834
  conn.commit()
835
 
836
- # ── 4. CẬP NHẬT RAM NGAY LẬP TỨC ─────────────────────────────────
837
- for pid, pname, prole, pimg, pexpiry, enc in new_encodings:
838
- face_memory_store.add(pid, pname, prole, pimg, enc) # Đã bỏ work_expiry_date
839
 
840
- logger.info(f"[Register] {name} | {len(new_encodings)} mẫu | RAM: {face_memory_store.count}")
841
  return {
842
  "success": True,
843
- "message": f"Đã đăng ký {name} với {len(new_encodings)} mẫu.",
844
- "img_url": f"/uploads/{Path(avatar_path).name}" if avatar_path else "",
845
  "ramCount": face_memory_store.count,
846
  }
847
 
848
  except Exception as e:
849
  conn.rollback()
850
  logger.error(f"[Register Lỗi] {e}")
851
- # Rollback: Xóa các file ảnh vừa tạo nếu có lỗi xảy ra
852
  for path in saved_files:
853
  p = Path(path)
854
  if p.exists():
@@ -857,8 +437,10 @@ async def register(
857
  finally:
858
  cursor.close()
859
  conn.close()
 
 
860
  # ═════════════════════════════════════════════════════════════════════════════
861
- # DANH SÁCH NGƯỜI DÙNG
862
  # ═════════════════════════════════════════════════════════════════════════════
863
  @app.get("/api/face/persons")
864
  async def get_persons():
@@ -867,7 +449,7 @@ async def get_persons():
867
  try:
868
  cursor.execute("""
869
  SELECT p.id, p.name, p.role, p.department, p.status,
870
- p.img_path, p.work_expiry_date,
871
  p.registered_at AS registered,
872
  (SELECT COUNT(*) FROM face_embeddings e WHERE e.person_id = p.id) AS embeddings,
873
  (SELECT COUNT(*) FROM recognition_logs l WHERE l.person_id = p.id AND l.status = 'success') AS recognitions,
@@ -881,8 +463,11 @@ async def get_persons():
881
  rows = cursor.fetchall()
882
  today = str(date.today())
883
  for row in rows:
884
- raw = row.get("img_path") or ""
885
- row["img"] = f"/uploads/{Path(raw).name}" if raw else ""
 
 
 
886
  exp = row.get("work_expiry_date")
887
  row["is_expired"] = bool(exp and str(exp) < today)
888
  return {"success": True, "data": rows, "total": len(rows), "ramCount": face_memory_store.count}
@@ -892,7 +477,7 @@ async def get_persons():
892
 
893
 
894
  # ═════════════════════════════════════════════════════════════════════════════
895
- # CẬP NHẬT & XÓA & LOGS & THỐNG (Giữ nguyên)
896
  # ═════════════════════════════════════════════════════════════════════════════
897
  @app.put("/api/face/persons/{id}")
898
  async def update_person(id: str, person_data: PersonUpdate):
@@ -941,18 +526,20 @@ async def get_logs():
941
  try:
942
  cursor.execute("""
943
  SELECT l.id, COALESCE(p.name, 'Người lạ') AS name,
944
- DATE_FORMAT(l.created_at, '%H:%i:%s') AS time,
945
- DATE_FORMAT(l.created_at, '%d/%m/%Y') AS date,
946
  l.status, l.confidence, l.camera, l.action,
947
- p.img_path AS img_raw
 
948
  FROM recognition_logs l
949
  LEFT JOIN persons p ON l.person_id = p.id
950
  ORDER BY l.created_at DESC LIMIT 100
951
  """)
952
  rows = cursor.fetchall()
953
  for row in rows:
 
954
  raw = row.pop("img_raw", "") or ""
955
- row["img"] = f"/uploads/{Path(raw).name}" if raw else ""
956
  return {"success": True, "data": rows, "total": len(rows)}
957
  finally:
958
  cursor.close()
@@ -963,7 +550,8 @@ async def get_statistics():
963
  conn = get_db_connection()
964
  cursor = conn.cursor(dictionary=True)
965
  try:
966
- cursor.execute("SELECT status, created_at FROM recognition_logs ORDER BY created_at DESC LIMIT 1000")
 
967
  all_logs = cursor.fetchall()
968
  hourly = {f"{i:02d}:00": {"nhận_diện": 0, "từ_chối": 0, "lạ": 0} for i in range(24)}
969
  days = ["T2", "T3", "T4", "T5", "T6", "T7", "CN"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import sys
3
  from pathlib import Path
4
+ import threading
5
+ import platform
6
+
7
+ # ─── 0. FIX LỖI PYTORCH TRÊN MÔI TRƯỜNG LINUX (HUGGING FACE) ──────────────────
8
+ if platform.system() == 'Linux':
9
+ import pathlib
10
+ pathlib.WindowsPath = pathlib.PosixPath
11
 
12
  # ─── 1. CẤU HÌNH ĐƯỜNG DẪN TUYỆT ĐỐI (TRÁNH LẠC ĐƯỜNG) ────���─────────────────
 
13
  current_dir = os.path.dirname(os.path.abspath(__file__))
 
14
  root_dir = os.path.dirname(current_dir)
15
 
 
16
  sys.path.insert(0, current_dir)
17
  sys.path.insert(0, os.path.join(current_dir, 'DetecInfoBoxes'))
18
  if root_dir not in sys.path:
 
29
  from datetime import date
30
  from dotenv import load_dotenv
31
 
32
+ # ─── 3. IMPORT CHUẨN ──────────────────────────────────────────────────────────
33
  from readInfoIdCard import ReadInfo
34
  from DetecInfoBoxes.GetBoxes import Detect
35
  from Vocr.tool.predictor import Predictor
 
51
  logging.basicConfig(level=logging.INFO)
52
  logger = logging.getLogger(__name__)
53
 
54
+ # BẮT BUỘC: Tạo thư mục uploads nếu chưa có
55
+ os.makedirs(UPLOAD_DIR, exist_ok=True)
56
 
57
+ # ─── KHỞI TẠO AI CHẠY NGẦM (CHỐNG TIMEOUT CLOUD) ──────────────────────────────
58
+ ocr_predictor = None
59
+ read_info = None
60
+ is_ai_ready = False
61
 
62
+ def load_ai_background():
63
+ global ocr_predictor, read_info, is_ai_ready
64
+ try:
65
+ logger.info("[AI_LOADER] Bắt đầu nạp mô hình VietOCR và YOLO chạy ngầm...")
66
+ vocr_config_path = os.path.join(current_dir, 'Vocr', 'config', 'vgg-seq2seq.yml')
67
+ config_vietocr = Cfg_vietocr.load_config_from_file(vocr_config_path)
68
+ config_vietocr['weights'] = os.path.join(current_dir, 'Models', 'seq2seqocr.pth')
69
+ config_vietocr['device'] = 'cpu'
70
+ ocr_predictor = Predictor(config_vietocr)
71
+
72
+ get_dictionary = Detect(opt)
73
+ scan_weight = os.path.join(current_dir, 'Models', 'cccdYoloV7.pt')
74
+ imgsz, stride, device, half, model, names = get_dictionary.load_model(scan_weight)
75
+
76
+ read_info = ReadInfo(imgsz, stride, device, half, model, names, ocr_predictor)
77
+ is_ai_ready = True
78
+ logger.info("[AI_LOADER] Hệ thống YOLO + VietOCR đã sẵn sàng!")
79
+ except Exception as e:
80
+ logger.error(f"[AI_LOADER] Lỗi khi nạp AI: {e}")
81
 
82
 
83
  # ─── Startup ──────────────────────────────────────────────────────────────────
 
88
  logger.info("[Startup] Nạp embedding vào RAM...")
89
  _load_embeddings_to_ram()
90
  logger.info(f"[Startup] {face_memory_store.count} khuôn mặt trên RAM")
91
+
92
+ # Bật luồng chạy ngầm để nạp AI
93
+ threading.Thread(target=load_ai_background, daemon=True).start()
94
  yield
95
  logger.info("[Shutdown] Bye!")
96
+
97
  def _load_embeddings_to_ram():
98
  conn = None
99
  cursor = None
 
101
  conn = get_db_connection()
102
  cursor = conn.cursor(dictionary=True)
103
  cursor.execute("""
104
+ SELECT e.person_id, p.name, p.role, p.img_path, p.img_url,
105
  p.work_expiry_date, e.embedding_vector
106
  FROM face_embeddings e
107
  JOIN persons p ON e.person_id = p.id
 
111
  parsed = []
112
  for row in rows:
113
  try:
114
+ # Ưu tiên lấy URL ảnh online truyền vào RAM, để khi nhận diện xong trả về React luôn
115
+ display_img = row.get("img_url") or row.get("img_path", "")
116
  parsed.append({
117
  "person_id": row["person_id"],
118
  "name": row["name"],
119
  "role": row.get("role", ""),
120
+ "img_path": display_img,
121
  "work_expiry_date": str(row["work_expiry_date"]) if row.get("work_expiry_date") else None,
122
  "embedding_vector": json.loads(row["embedding_vector"]),
123
  })
124
  except Exception as e:
125
  logger.warning(f"[Startup] Bỏ qua khuôn mặt lỗi: {e}")
126
+ face_memory_store.load_all(parsed)
127
 
128
  except Exception as e:
129
+ logger.error(f"[Startup] Lỗi kết nối DB khi nạp dữ liệu: {e}")
130
  face_memory_store.load_all([])
131
 
132
  finally:
 
147
 
148
  app.mount("/uploads", StaticFiles(directory=UPLOAD_DIR), name="uploads")
149
 
 
150
  class PersonUpdate(BaseModel):
151
  name: str
152
  role: str
153
  department: str
154
 
 
155
  def save_log_to_db(log_queries: list) -> None:
156
  if not log_queries:
157
  return
 
169
  logger.error(f"[Log] {e}")
170
 
171
 
172
+ # ═════════════════════════════════════════════════════════════════════════════
173
+ # API ĐỌC CCCD (OCR)
174
+ # ═════════════════════════════════════════════════════════════════════════════
175
  @app.post("/api/face/ocr")
176
  async def extract_ocr_local(file: UploadFile = File(...), side: str = Form(...)):
177
+ global is_ai_ready, read_info
178
+ if not is_ai_ready or not read_info:
179
+ return JSONResponse(status_code=503, content={"success": False, "error": "AI đang khởi động, vui lòng chờ 1 phút rồi thử lại!"})
180
+
181
  temp_path = ""
182
  try:
183
  temp_filename = f"temp_cccd_{uuid.uuid4().hex}.jpg"
 
186
  with open(temp_path, "wb") as f:
187
  f.write(file_bytes)
188
 
189
+ logger.info(f"[OCR] Phân tích mặt {side}...")
190
 
191
  if side == "front":
192
  raw = read_info.get_all_info(temp_path)
 
193
  mapped_data = {
194
  "id_number": raw.get("id", ""),
195
  "full_name": raw.get("full_name", ""),
 
200
  "address": raw.get("place_of_residence", ""),
201
  "expiry_date": raw.get("date_of_expiry", ""),
202
  }
203
+ else:
 
204
  raw = read_info.get_back_info(temp_path)
 
205
  mapped_data = {
206
  "issue_date": raw.get("issue_date", ""),
207
  "issued_by": raw.get("issued_by", ""),
 
211
  if os.path.exists(temp_path):
212
  os.remove(temp_path)
213
 
 
214
  return {"success": True, "data": mapped_data}
215
 
216
  except Exception as e:
217
+ logger.error(f"[OCR] Lỗi: {e}")
218
  if os.path.exists(temp_path):
219
  os.remove(temp_path)
220
  return {"success": True, "data": {}}
221
 
222
 
223
  # ═════════════════════════════════════════════════════════════════════════════
224
+ # API NHẬN DIỆN KHUÔN MẶT
225
  # ═════════════════════════════════════════════════════════════════════════════
226
  @app.post("/api/face/recognize")
227
  async def recognize(
 
243
  match = face_memory_store.find_best_match(np.array(face["descriptor"], dtype=np.float32))
244
 
245
  if match:
 
246
  expiry_str = match.get("work_expiry_date")
247
  if expiry_str:
248
  if date.fromisoformat(expiry_str) < today:
 
249
  results.append({
250
  "id": match["person_id"], "name": match["name"],
251
  "role": match["role"], "img": "",
 
256
  continue
257
 
258
  confidence = round(max(0.0, (1.0 - match["distance"]) * 100.0), 2)
259
+
260
+ # Ưu tiên lấy Link Online ( RAM đang lưu URL online thay vì path)
261
+ img_url = match.get("img_path", "")
262
+ if img_url and not img_url.startswith("http"):
263
+ img_url = f"/uploads/{Path(img_url).name}"
264
+
265
  results.append({
266
  "id": match["person_id"], "name": match["name"],
267
  "role": match["role"], "img": img_url,
 
290
 
291
 
292
  # ═════════════════════════════════════════════════════════════════════════════
293
+ # API ĐĂNG KÝ
294
  # ═════════════════════════════════════════════════════════════════════════════
295
  @app.post("/api/face/register")
296
  async def register(
297
  name: str = Form(...),
298
  role: str = Form(""),
299
  department: str = Form(""),
300
+ work_expiry_date: str = Form(""),
301
+ cccd_info: str = Form("{}"),
302
  images: list[UploadFile] = File(...),
303
  cccd_front: UploadFile = File(None),
304
  cccd_back: UploadFile = File(None),
 
307
  cursor = conn.cursor()
308
  person_id = str(uuid.uuid4())
309
  new_encodings: list[tuple] = []
310
+ saved_files = []
311
+
312
+ # URL và Path để lưu cho người dùng
313
  avatar_path = ""
314
+ avatar_url = ""
315
 
 
316
  COSINE_THRESHOLD = 0.4
317
 
318
  try:
 
320
  expiry_val = work_expiry_date or None
321
  cccd_number = cccd.get("id_number")
322
 
323
+ # ── 1. CHECK TRÙNG CCCD ──
324
  if cccd_number:
325
  cursor.execute("SELECT id FROM citizen_ids WHERE id_number = %s", (cccd_number,))
326
  if cursor.fetchone():
327
  raise Exception("Số CCCD này đã được đăng ký trong hệ thống!")
328
 
329
+ # ── 2. LƯU ẢNH KHUÔN MẶT ──
330
  user_descriptor = None
331
  for i, img_file in enumerate(images):
332
  img_bytes = await img_file.read()
 
341
  emb_id = str(uuid.uuid4())
342
 
343
  if i == 0:
344
+ user_descriptor = descriptor
345
 
346
+ # NHẬN 2 KẾT QUẢ TỪ HÀM SAVE_IMAGE MỚI
347
+ saved_path, saved_url = face_ai_service.save_image(img_bytes, person_id, index=i)
348
  saved_files.append(saved_path)
349
 
350
  if i == 0:
351
  avatar_path = saved_path
352
+ avatar_url = saved_url
353
  cursor.execute(
354
  """INSERT INTO persons
355
+ (id, name, role, department, status, img_path, img_url, work_expiry_date)
356
+ VALUES (%s, %s, %s, %s, 'active', %s, %s, %s)""",
357
+ (person_id, name, role, department, avatar_path, avatar_url, expiry_val),
358
  )
359
 
360
  cursor.execute(
361
  "INSERT INTO face_embeddings (id, person_id, embedding_vector) VALUES (%s, %s, %s)",
362
  (emb_id, person_id, json.dumps(descriptor)),
363
  )
364
+
365
+ # Truyền URL online vào RAM nếu có, không có thì dùng path cũ
366
+ display_img = avatar_url if avatar_url else avatar_path
367
+ new_encodings.append((person_id, name, role, display_img, descriptor))
368
 
369
+ # ── 3. LƯU ẢNH CCCD ──
370
  front_path, back_path = "", ""
371
 
372
  if cccd_front:
373
  fb_bytes = await cccd_front.read()
374
  if fb_bytes:
 
375
  cccd_detections = face_ai_service.extract_faces(fb_bytes)
376
  if len(cccd_detections) == 0:
377
  raise Exception("Không tìm thấy khuôn mặt trên ảnh mặt trước CCCD.")
378
 
379
  cccd_descriptor = cccd_detections[0]["descriptor"]
 
 
380
  q = face_memory_store._norm(np.array(user_descriptor, dtype=np.float32))
381
  c = face_memory_store._norm(np.array(cccd_descriptor, dtype=np.float32))
382
  score = float(np.dot(q, c))
 
385
  logger.warning(f"Cảnh báo giả mạo: Score {score} < {COSINE_THRESHOLD}")
386
  raise Exception("Cảnh báo: Khuôn mặt trên thẻ CCCD KHÔNG KHỚP với ảnh chụp trực tiếp!")
387
 
388
+ f_path, f_url = face_ai_service.save_image(fb_bytes, f"cccd_front_{person_id}", index=0)
389
+ front_path = f_path
390
  saved_files.append(front_path)
391
 
392
  if cccd_back:
393
  bb_bytes = await cccd_back.read()
394
  if bb_bytes:
395
+ b_path, b_url = face_ai_service.save_image(bb_bytes, f"cccd_back_{person_id}", index=0)
396
+ back_path = b_path
397
  saved_files.append(back_path)
398
 
399
  cursor.execute("""
 
415
 
416
  conn.commit()
417
 
418
+ # ── 4. CẬP NHẬT RAM NGAY LẬP TỨC ──
419
+ for pid, pname, prole, pimg, enc in new_encodings:
420
+ face_memory_store.add(pid, pname, prole, pimg, enc)
421
 
 
422
  return {
423
  "success": True,
424
+ "message": f"Đã đăng ký {name} thành công.",
425
+ "img_url": avatar_url if avatar_url else (f"/uploads/{Path(avatar_path).name}" if avatar_path else ""),
426
  "ramCount": face_memory_store.count,
427
  }
428
 
429
  except Exception as e:
430
  conn.rollback()
431
  logger.error(f"[Register Lỗi] {e}")
 
432
  for path in saved_files:
433
  p = Path(path)
434
  if p.exists():
 
437
  finally:
438
  cursor.close()
439
  conn.close()
440
+
441
+
442
  # ═════════════════════════════════════════════════════════════════════════════
443
+ # API LẤY DANH SÁCH (SỬA ĐỂ TRẢ VỀ LINK IMGBB)
444
  # ═════════════════════════════════════════════════════════════════════════════
445
  @app.get("/api/face/persons")
446
  async def get_persons():
 
449
  try:
450
  cursor.execute("""
451
  SELECT p.id, p.name, p.role, p.department, p.status,
452
+ p.img_path, p.img_url, p.work_expiry_date,
453
  p.registered_at AS registered,
454
  (SELECT COUNT(*) FROM face_embeddings e WHERE e.person_id = p.id) AS embeddings,
455
  (SELECT COUNT(*) FROM recognition_logs l WHERE l.person_id = p.id AND l.status = 'success') AS recognitions,
 
463
  rows = cursor.fetchall()
464
  today = str(date.today())
465
  for row in rows:
466
+ # Lấy URL online trước, nếu không có mới chế link Local
467
+ online_link = row.get("img_url")
468
+ local_path = row.get("img_path") or ""
469
+ row["img"] = online_link if online_link else (f"/uploads/{Path(local_path).name}" if local_path else "")
470
+
471
  exp = row.get("work_expiry_date")
472
  row["is_expired"] = bool(exp and str(exp) < today)
473
  return {"success": True, "data": rows, "total": len(rows), "ramCount": face_memory_store.count}
 
477
 
478
 
479
  # ═════════════════════════════════════════════════════════════════════════════
480
+ # CẬP NHẬT, XÓA LOGS (FIX MÚI GIỜ + URL)
481
  # ═════════════════════════════════════════════════════════════════════════════
482
  @app.put("/api/face/persons/{id}")
483
  async def update_person(id: str, person_data: PersonUpdate):
 
526
  try:
527
  cursor.execute("""
528
  SELECT l.id, COALESCE(p.name, 'Người lạ') AS name,
529
+ DATE_FORMAT(DATE_ADD(l.created_at, INTERVAL 7 HOUR), '%H:%i:%s') AS time,
530
+ DATE_FORMAT(DATE_ADD(l.created_at, INTERVAL 7 HOUR), '%d/%m/%Y') AS date,
531
  l.status, l.confidence, l.camera, l.action,
532
+ p.img_path AS img_raw,
533
+ p.img_url
534
  FROM recognition_logs l
535
  LEFT JOIN persons p ON l.person_id = p.id
536
  ORDER BY l.created_at DESC LIMIT 100
537
  """)
538
  rows = cursor.fetchall()
539
  for row in rows:
540
+ online_link = row.pop("img_url", None)
541
  raw = row.pop("img_raw", "") or ""
542
+ row["img"] = online_link if online_link else (f"/uploads/{Path(raw).name}" if raw else "")
543
  return {"success": True, "data": rows, "total": len(rows)}
544
  finally:
545
  cursor.close()
 
550
  conn = get_db_connection()
551
  cursor = conn.cursor(dictionary=True)
552
  try:
553
+ # CỘNG 7 TIẾNG ĐỂ BIỂU ĐỒ HIỂN THỊ ĐÚNG GIỜ VIỆT NAM
554
+ cursor.execute("SELECT status, DATE_ADD(created_at, INTERVAL 7 HOUR) AS created_at FROM recognition_logs ORDER BY created_at DESC LIMIT 1000")
555
  all_logs = cursor.fetchall()
556
  hourly = {f"{i:02d}:00": {"nhận_diện": 0, "từ_chối": 0, "lạ": 0} for i in range(24)}
557
  days = ["T2", "T3", "T4", "T5", "T6", "T7", "CN"]