#!/usr/bin/env php PDO::ERRMODE_EXCEPTION]); } catch (Throwable $e) { echo "Koneksi DB gagal: " . $e->getMessage() . "\n"; exit(1); } $facesDir = $backendRoot . DIRECTORY_SEPARATOR . 'writable' . DIRECTORY_SEPARATOR . 'faces'; if (! is_dir($facesDir)) { mkdir($facesDir, 0755, true); echo "Folder dibuat: {$facesDir}\n"; } $faceServiceUrl = rtrim($env['FACE_SERVICE_URL'] ?? $env['face_service_url'] ?? 'http://localhost:5000', '/'); $embeddingDim = (int) ($env['FACE_EMBEDDING_DIM'] ?? $env['face_embedding_dim'] ?? 512); $minFaceSize = (float) ($env['FACE_MIN_SIZE'] ?? $env['face_min_size'] ?? 80); $minBlur = (float) ($env['FACE_MIN_BLUR'] ?? $env['face_min_blur'] ?? 30); $minBrightness = (float) ($env['FACE_MIN_BRIGHTNESS'] ?? $env['face_min_brightness'] ?? 0.2); /** * Generate embedding dari file gambar lalu simpan ke student_faces (source=formal). * Menggantikan embedding formal yang sudah ada untuk student_id ini. * Return: 'ok' | 'skip' | 'fail' */ $generateAndSaveEmbedding = function (PDO $pdo, string $destPath, int $studentId) use ($faceServiceUrl, $embeddingDim, $minFaceSize, $minBlur, $minBrightness): string { if (! is_file($destPath)) { return 'fail'; } $mime = mime_content_type($destPath) ?: 'image/jpeg'; $ch = curl_init($faceServiceUrl . '/embed'); if ($ch === false) { return 'fail'; } $cfile = new CURLFile($destPath, $mime, basename($destPath)); curl_setopt_array($ch, [ CURLOPT_POST => 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), RecursiveIteratorIterator::SELF_FIRST ); foreach ($it as $file) { if (! $file->isFile()) { continue; } $path = $file->getPathname(); $ext = strtolower($file->getExtension()); if (! in_array($ext, $extensions, true)) { continue; } // Nama file bisa berupa "1234567890.jpg" atau "01. 1234567890.jpg" $baseName = preg_replace('/\.(jpg|jpeg|png)$/i', '', $file->getFilename()); $baseName = trim($baseName); if ($baseName === '') { continue; } // Ambil deretan digit minimal 8 angka pertama sebagai NISN $nisn = null; if (preg_match('/(\d{8,})/', $baseName, $m) === 1) { $nisn = $m[1]; } if ($nisn === null || $nisn === '') { echo " [skip] Tidak ditemukan NISN di nama file: {$baseName} ({$path})\n"; $countSkip++; continue; } $stmt = $pdo->prepare('SELECT id FROM students WHERE nisn = ? LIMIT 1'); $stmt->execute([$nisn]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (! $row) { echo " [skip] NISN tidak ada di DB: {$nisn} ({$path})\n"; $countSkip++; continue; } $studentId = (int) $row['id']; $dest = $facesDir . DIRECTORY_SEPARATOR . $studentId . '.jpg'; // Resize + kompres dengan GD: lebar max 600px, quality 75 $srcImage = null; if ($ext === 'jpg' || $ext === 'jpeg') { $srcImage = @imagecreatefromjpeg($path); } elseif ($ext === 'png') { $srcImage = @imagecreatefrompng($path); } if ($srcImage === false || $srcImage === null) { // Fallback: langsung copy jika GD gagal if (copy($path, $dest)) { $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}"; $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++; } continue; } $width = imagesx($srcImage); $height = imagesy($srcImage); $maxWidth = 600; if ($width > $maxWidth) { $ratio = $maxWidth / $width; $newWidth = $maxWidth; $newHeight = (int) round($height * $ratio); } else { $newWidth = $width; $newHeight = $height; } $dstImage = imagecreatetruecolor($newWidth, $newHeight); imagecopyresampled($dstImage, $srcImage, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height); if (imagejpeg($dstImage, $dest, 75)) { imagedestroy($srcImage); imagedestroy($dstImage); $hash = md5_file($dest); $update = $pdo->prepare('UPDATE students SET face_hash = ? WHERE id = ?'); $update->execute([$hash, $studentId]); 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); echo " [fail] Gagal save JPG: {$path} -> {$dest}\n"; $countFail++; } } 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"; }