106 lines
3.7 KiB
PHP
106 lines
3.7 KiB
PHP
<?php
|
|
|
|
namespace App\Modules\Attendance\Controllers;
|
|
|
|
use App\Core\BaseApiController;
|
|
use App\Modules\Face\Models\StudentFaceModel;
|
|
use App\Modules\Face\Services\FaceService;
|
|
use CodeIgniter\HTTP\ResponseInterface;
|
|
|
|
/**
|
|
* FaceVerifyController
|
|
*
|
|
* POST /api/attendance/verify-face
|
|
* Body: { "student_id": 123, "image": "data:image/jpeg;base64,..." }
|
|
*
|
|
* Strategi: Option 1 — bandingkan probe hanya dengan embedding milik student_id kandidat.
|
|
*/
|
|
class FaceVerifyController extends BaseApiController
|
|
{
|
|
public function verify(): ResponseInterface
|
|
{
|
|
$payload = $this->request->getJSON(true) ?? [];
|
|
$studentId = (int) ($payload['student_id'] ?? 0);
|
|
$imageData = (string) ($payload['image'] ?? '');
|
|
|
|
if ($studentId < 1) {
|
|
return $this->errorResponse('student_id wajib diisi', null, null, ResponseInterface::HTTP_UNPROCESSABLE_ENTITY);
|
|
}
|
|
if ($imageData === '') {
|
|
return $this->errorResponse('image (base64) wajib diisi', null, null, ResponseInterface::HTTP_UNPROCESSABLE_ENTITY);
|
|
}
|
|
|
|
$tmpFile = tempnam(sys_get_temp_dir(), 'probe_');
|
|
|
|
try {
|
|
$raw = $this->decodeBase64Image($imageData);
|
|
if ($raw === null) {
|
|
throw new \RuntimeException('Format gambar tidak valid (harus base64)');
|
|
}
|
|
file_put_contents($tmpFile, $raw);
|
|
|
|
$faceService = new FaceService();
|
|
$probe = $faceService->extractEmbeddingWithQuality($tmpFile)['embedding'];
|
|
|
|
$faceModel = new StudentFaceModel();
|
|
$rows = $faceModel->where('student_id', $studentId)->findAll();
|
|
if (empty($rows)) {
|
|
return $this->errorResponse('Belum ada embedding wajah untuk siswa ini', null, null, ResponseInterface::HTTP_NOT_FOUND);
|
|
}
|
|
|
|
$bestSim = -1.0;
|
|
$bestSource = null;
|
|
|
|
foreach ($rows as $row) {
|
|
$embedding = json_decode($row['embedding'] ?? '[]', true);
|
|
if (! is_array($embedding) || $embedding === []) {
|
|
continue;
|
|
}
|
|
$sim = $faceService->cosineSimilarity($probe, array_map('floatval', $embedding));
|
|
if ($sim > $bestSim) {
|
|
$bestSim = $sim;
|
|
$bestSource = $row['source'] ?? null;
|
|
}
|
|
}
|
|
|
|
if ($bestSim < 0) {
|
|
return $this->errorResponse('Tidak ada embedding valid untuk siswa ini', null, null, ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
|
|
}
|
|
|
|
$threshold = $faceService->getDefaultThreshold();
|
|
$status = $bestSim >= $threshold ? 'match' : 'no_match';
|
|
|
|
$data = [
|
|
'student_id' => $studentId,
|
|
'similarity' => $bestSim,
|
|
'threshold' => $threshold,
|
|
'matched_source'=> $bestSource,
|
|
'status' => $status,
|
|
];
|
|
|
|
return $this->successResponse($data, 'Face verification processed');
|
|
} catch (\Throwable $e) {
|
|
return $this->errorResponse('Gagal verifikasi wajah: ' . $e->getMessage(), null, null, ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
|
|
} finally {
|
|
if (is_file($tmpFile)) {
|
|
@unlink($tmpFile);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function decodeBase64Image(string $input): ?string
|
|
{
|
|
$input = trim($input);
|
|
if ($input === '') {
|
|
return null;
|
|
}
|
|
if (strpos($input, 'base64,') !== false) {
|
|
$parts = explode('base64,', $input, 2);
|
|
$input = $parts[1];
|
|
}
|
|
$data = base64_decode($input, true);
|
|
return $data === false ? null : $data;
|
|
}
|
|
}
|
|
|