From 185b00df7c8c48cb76c248f7144d825acdb59358 Mon Sep 17 00:00:00 2001 From: mwpn Date: Sun, 8 Mar 2026 14:28:39 +0700 Subject: [PATCH] Sync foto: generate embedding dan simpan ke student_faces, opsi no-embedding, update docs --- docs/SYNC_FOTO_WAJAH.md | 11 ++++- scripts/sync_face_photos.php | 96 ++++++++++++++++++++++++++++++++++-- 2 files changed, 100 insertions(+), 7 deletions(-) diff --git a/docs/SYNC_FOTO_WAJAH.md b/docs/SYNC_FOTO_WAJAH.md index 2b2a95c..458da7e 100644 --- a/docs/SYNC_FOTO_WAJAH.md +++ b/docs/SYNC_FOTO_WAJAH.md @@ -2,6 +2,8 @@ Foto wajah siswa dipakai untuk **verifikasi wajah** di aplikasi mobile (absen masuk/pulang). Foto disimpan di backend: `writable/faces/{student_id}.jpg`. +**Alur yang proper:** Verifikasi wajah memakai **vektor (embedding)** yang disimpan di tabel `student_faces`, bukan file gambar mentah. Jadi setelah foto formal ada (dari Drive atau sumber lain), kita **harus bikin vektor dulu** lewat face-service lalu simpan ke `student_faces`. Script sync di bawah mengerjakan keduanya: copy file **dan** generate embedding. + ## Sumber foto Folder contoh: [Foto Siswa Kls XII TP 25-26](https://drive.google.com/drive/folders/16E84NFGYPItaTANQwEyMQW5rkTCt6Asy) — berisi subfolder per kelas (XII-1, XII-2, … XII-12, Susulan). @@ -23,11 +25,14 @@ Folder contoh: [Foto Siswa Kls XII TP 25-26](https://drive.google.com/drive/fold cd c:\laragon\www\sman1\backend php scripts/sync_face_photos.php --source=C:\temp\foto-siswa-xii ``` + - Pastikan **face-service** (Python) sudah jalan, karena script akan memanggilnya untuk generate embedding. - Script akan: - Scan semua subfolder (XII-1, XII-2, dll.), - Untuk setiap file gambar (.jpg, .jpeg, .png), ambil nama file tanpa ekstensi = NISN, - Cari siswa di database berdasarkan NISN, - - Copy file ke `writable/faces/{student_id}.jpg`. + - Copy file ke `writable/faces/{student_id}.jpg`, + - **Kirim gambar ke face-service → dapat embedding → simpan ke tabel `student_faces` (source=formal)**. + - Opsi `--no-embedding`: hanya copy file + update `face_hash`, tidak generate vektor (misalnya kalau face-service belum jalan). ## Cara 2: Upload per siswa lewat dashboard (rencana) @@ -44,5 +49,7 @@ Ke depan bisa ditambah halaman di **Pengaturan Academic → Siswa**: per siswa a | Yang diatur | Keterangan | |-------------|------------| | Penyimpanan | `writable/faces/{student_id}.jpg` (atau .png) | +| **Vektor (embedding)** | Script sync memanggil face-service lalu menyimpan ke `student_faces` (source=formal). Verifikasi wajah memakai data ini. | | Mapping | Nama file = NISN → cari siswa → simpan dengan student_id | -| Sumber | Download folder Drive → rename file = NISN → jalankan script sync | +| Sumber | Download folder Drive → rename file = NISN → jalankan script sync (dan pastikan face-service jalan) | +| Opsi | `--no-embedding` = hanya copy file, tidak generate embedding | diff --git a/scripts/sync_face_photos.php b/scripts/sync_face_photos.php index 4a8103f..468a461 100644 --- a/scripts/sync_face_photos.php +++ b/scripts/sync_face_photos.php @@ -2,15 +2,19 @@ true, + CURLOPT_POSTFIELDS => ['image' => $cfile], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTPHEADER => [], + ]); + $response = curl_exec($ch); + $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($response === false || $status !== 200) { + return 'fail'; + } + $data = json_decode($response, true); + if (! is_array($data) || empty($data['embedding']) || ! is_array($data['embedding'])) { + return 'fail'; + } + $embedding = $data['embedding']; + if (count($embedding) !== $embeddingDim) { + return 'skip'; + } + $facesCount = (int) ($data['faces_count'] ?? 0); + $faceSize = (float) ($data['face_size'] ?? 0); + $blur = (float) ($data['blur'] ?? 0); + $brightness = (float) ($data['brightness'] ?? 0); + if ($facesCount !== 1) { + return 'skip'; + } + if ($faceSize < $minFaceSize || $blur < $minBlur || $brightness < $minBrightness) { + return 'skip'; + } + $qualityScore = isset($data['quality_score']) ? (float) $data['quality_score'] : null; + $embeddingJson = json_encode(array_map('floatval', $embedding)); + + $pdo->prepare('DELETE FROM student_faces WHERE student_id = ? AND source = ?')->execute([$studentId, 'formal']); + $ins = $pdo->prepare('INSERT INTO student_faces (student_id, embedding, source, quality_score, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())'); + $ins->execute([$studentId, $embeddingJson, 'formal', $qualityScore]); + return 'ok'; +}; + $extensions = ['jpg', 'jpeg', 'png']; $countOk = 0; $countSkip = 0; $countFail = 0; +$countEmbedOk = 0; +$countEmbedSkip = 0; +$countEmbedFail = 0; $it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS), @@ -125,8 +194,15 @@ foreach ($it as $file) { $hash = md5_file($dest); $update = $pdo->prepare('UPDATE students SET face_hash = ? WHERE id = ?'); $update->execute([$hash, $studentId]); - echo " [ok-copy] {$nisn} -> student_id {$studentId}\n"; + echo " [ok-copy] {$nisn} -> student_id {$studentId}"; $countOk++; + if (! $noEmbedding) { + $emb = $generateAndSaveEmbedding($pdo, $dest, $studentId); + if ($emb === 'ok') { $countEmbedOk++; echo ' + embedding'; } + elseif ($emb === 'skip') { $countEmbedSkip++; echo ' (embed skip)'; } + else { $countEmbedFail++; echo ' (embed fail)'; } + } + echo "\n"; } else { echo " [fail] Gagal copy (GD error): {$path} -> {$dest}\n"; $countFail++; @@ -155,8 +231,15 @@ foreach ($it as $file) { $hash = md5_file($dest); $update = $pdo->prepare('UPDATE students SET face_hash = ? WHERE id = ?'); $update->execute([$hash, $studentId]); - echo " [ok] {$nisn} -> student_id {$studentId}\n"; + echo " [ok] {$nisn} -> student_id {$studentId}"; $countOk++; + if (! $noEmbedding) { + $emb = $generateAndSaveEmbedding($pdo, $dest, $studentId); + if ($emb === 'ok') { $countEmbedOk++; echo ' + embedding'; } + elseif ($emb === 'skip') { $countEmbedSkip++; echo ' (embed skip)'; } + else { $countEmbedFail++; echo ' (embed fail)'; } + } + echo "\n"; } else { imagedestroy($srcImage); imagedestroy($dstImage); @@ -166,3 +249,6 @@ foreach ($it as $file) { } echo "\nSelesai. OK: {$countOk}, Skip (NISN tidak ada): {$countSkip}, Gagal: {$countFail}\n"; +if (! $noEmbedding) { + echo "Embedding: OK: {$countEmbedOk}, Skip (kualitas/tidak 1 wajah): {$countEmbedSkip}, Gagal: {$countEmbedFail}\n"; +}