Files
presensi/app/Modules/Face/Controllers/FaceController.php
2026-03-05 14:37:36 +07:00

237 lines
7.4 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Modules\Face\Controllers;
use App\Core\BaseApiController;
use App\Modules\Academic\Models\StudentModel;
use App\Modules\Face\Models\StudentFaceModel;
use App\Modules\Face\Services\FaceService;
use CodeIgniter\HTTP\ResponseInterface;
class FaceController extends BaseApiController
{
/**
* POST /api/face/import-formal
* Body: {
* "items": [
* { "student_id": 123, "url": "https://..." },
* ...
* ]
* }
*
* Admin-only (protected by filter admin_only).
*/
public function importFormal(): ResponseInterface
{
$payload = $this->request->getJSON(true) ?? [];
$items = $payload['items'] ?? [];
if (! is_array($items) || $items === []) {
return $this->errorResponse('items wajib berupa array', null, null, ResponseInterface::HTTP_UNPROCESSABLE_ENTITY);
}
$studentModel = new StudentModel();
$faceModel = new StudentFaceModel();
$faceService = new FaceService();
$success = 0;
$fail = 0;
$errors = [];
foreach ($items as $item) {
$studentId = (int) ($item['student_id'] ?? 0);
$url = trim((string) ($item['url'] ?? ''));
if ($studentId < 1 || $url === '') {
$fail++;
$errors[] = [
'student_id' => $studentId,
'url' => $url,
'reason' => 'student_id dan url wajib diisi',
];
continue;
}
$student = $studentModel->find($studentId);
if (! $student) {
$fail++;
$errors[] = [
'student_id' => $studentId,
'url' => $url,
'reason' => 'Siswa tidak ditemukan',
];
continue;
}
$tmpFile = tempnam(sys_get_temp_dir(), 'face_');
try {
$imgData = @file_get_contents($url);
if ($imgData === false) {
throw new \RuntimeException('Gagal mengunduh gambar dari URL');
}
file_put_contents($tmpFile, $imgData);
$result = $faceService->extractEmbeddingWithQuality($tmpFile);
$faceModel->insert([
'student_id' => $studentId,
'embedding' => json_encode($result['embedding']),
'source' => 'formal',
'quality_score' => $result['quality_score'],
]);
// Simpan URL formal di tabel students
$studentModel->update($studentId, ['photo_formal_url' => $url]);
$success++;
} catch (\Throwable $e) {
$fail++;
$errors[] = [
'student_id' => $studentId,
'url' => $url,
'reason' => $e->getMessage(),
];
} finally {
if (is_file($tmpFile)) {
@unlink($tmpFile);
}
}
}
return $this->successResponse([
'success_count' => $success,
'fail_count' => $fail,
'errors' => $errors,
], 'Import foto formal selesai');
}
/**
* POST /api/face/enroll-live
* Body: {
* "student_id": 123,
* "images": ["data:image/jpeg;base64,...", "..."]
* }
*
* Dipanggil saat aktivasi dari device (35 frame).
*/
public function enrollLive(): ResponseInterface
{
$payload = $this->request->getJSON(true) ?? [];
$studentId = (int) ($payload['student_id'] ?? 0);
$images = $payload['images'] ?? [];
if ($studentId < 1) {
return $this->errorResponse('student_id wajib diisi', null, null, ResponseInterface::HTTP_UNPROCESSABLE_ENTITY);
}
if (! is_array($images) || count($images) < 1) {
return $this->errorResponse('images wajib berisi minimal 1 item', null, null, ResponseInterface::HTTP_UNPROCESSABLE_ENTITY);
}
$studentModel = new StudentModel();
$student = $studentModel->find($studentId);
if (! $student) {
return $this->errorResponse('Siswa tidak ditemukan', null, null, ResponseInterface::HTTP_NOT_FOUND);
}
$faceModel = new StudentFaceModel();
$faceService = new FaceService();
$embeddings = [];
$saved = 0;
$errors = [];
foreach ($images as $idx => $imgData) {
$tmpFile = tempnam(sys_get_temp_dir(), 'live_');
try {
$raw = $this->decodeBase64Image($imgData);
if ($raw === null) {
throw new \RuntimeException('Format gambar tidak valid (harus base64)');
}
file_put_contents($tmpFile, $raw);
$result = $faceService->extractEmbeddingWithQuality($tmpFile);
$embeddings[] = $result['embedding'];
$faceModel->insert([
'student_id' => $studentId,
'embedding' => json_encode($result['embedding']),
'source' => 'live',
'quality_score' => $result['quality_score'],
]);
$saved++;
} catch (\Throwable $e) {
$errors[] = [
'index' => $idx,
'reason' => $e->getMessage(),
];
} finally {
if (is_file($tmpFile)) {
@unlink($tmpFile);
}
}
}
// Optional: simpan embedding rata-rata sebagai live_avg jika ada cukup sample
if (count($embeddings) >= 2) {
$avg = $this->averageEmbedding($embeddings);
$faceModel->insert([
'student_id' => $studentId,
'embedding' => json_encode($avg),
'source' => 'live_avg',
'quality_score' => null,
]);
}
return $this->successResponse([
'student_id' => $studentId,
'saved' => $saved,
'errors' => $errors,
], 'Enrollment live selesai');
}
/**
* Hitung rata-rata beberapa embedding (elemen per elemen).
*
* @param array<int, array<int,float>> $vectors
* @return float[]
*/
protected function averageEmbedding(array $vectors): array
{
$count = count($vectors);
if ($count === 0) {
return [];
}
$dim = count($vectors[0]);
$sum = array_fill(0, $dim, 0.0);
foreach ($vectors as $vec) {
for ($i = 0; $i < $dim; $i++) {
$sum[$i] += (float) $vec[$i];
}
}
for ($i = 0; $i < $dim; $i++) {
$sum[$i] /= $count;
}
return $sum;
}
/**
* Decode string base64 (data URL atau plain base64) ke binary.
*/
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;
}
}