240 lines
9.3 KiB
JavaScript
Executable File
240 lines
9.3 KiB
JavaScript
Executable File
|
|
import express from 'express';
|
|
import dotenv from 'dotenv';
|
|
|
|
dotenv.config();
|
|
|
|
const router = express.Router();
|
|
|
|
// Gemini API Base URL - Using REST API directly to avoid SDK issues
|
|
const GEMINI_API_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent';
|
|
|
|
const getApiKey = () => {
|
|
const key = process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY;
|
|
if (!key) throw new Error("Server GOOGLE_API_KEY or GEMINI_API_KEY is missing.");
|
|
return key;
|
|
};
|
|
|
|
// POST /api/ai/analyze - Analyze Face Quality
|
|
router.post('/analyze', async (req, res) => {
|
|
try {
|
|
const { image } = req.body;
|
|
if (!image) throw new Error("Image data missing");
|
|
|
|
const apiKey = getApiKey();
|
|
const base64Data = image.replace(/^data:image\/\w+;base64,/, "");
|
|
|
|
// Detect image type from data URI
|
|
let mimeType = 'image/jpeg';
|
|
if (image.startsWith('data:image/png')) {
|
|
mimeType = 'image/png';
|
|
} else if (image.startsWith('data:image/webp')) {
|
|
mimeType = 'image/webp';
|
|
}
|
|
|
|
const prompt = `Anda adalah sistem keamanan biometrik sekolah yang SANGAT KETAT.
|
|
|
|
Tugas: Analisis keaslian foto untuk absensi.
|
|
|
|
KRITERIA TOLAK MUTLAK (SPOOFING):
|
|
1. Terdeteksi sebagai foto dari layar HP/Laptop (Cari pola Moire/Pixel grid/Glare layar).
|
|
2. Terdeteksi sebagai foto cetak (kertas) atau pas foto yang difoto ulang.
|
|
3. Terdeteksi bingkai HP atau jari yang memegang HP lain di dalam foto.
|
|
4. Wajah tidak menghadap kamera secara langsung (Terlalu miring/profil).
|
|
5. Wajah tertutup masker, helm, atau kacamata hitam gelap.
|
|
|
|
PENTING: Berikan respons HANYA dalam format JSON yang valid, tanpa markdown atau backticks:
|
|
{"hasFace": true/false, "confidence": 0-100, "description": "penjelasan singkat"}
|
|
|
|
hasFace harus TRUE hanya jika foto selfie ASLI, LIVE, JELAS, dan WAJAH TUNGGAL.
|
|
Jika false, jelaskan alasannya di description dalam Bahasa Indonesia.`;
|
|
|
|
console.log("Calling Gemini REST API for face analysis...");
|
|
console.log("Using MIME type:", mimeType);
|
|
console.log("Base64 length:", base64Data.length);
|
|
|
|
const requestBody = {
|
|
contents: [{
|
|
parts: [
|
|
{
|
|
inline_data: {
|
|
mime_type: mimeType,
|
|
data: base64Data
|
|
}
|
|
},
|
|
{ text: prompt }
|
|
]
|
|
}]
|
|
};
|
|
|
|
const response = await fetch(`${GEMINI_API_URL}?key=${apiKey}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(requestBody)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error("Gemini API HTTP Error:", response.status, errorText);
|
|
throw new Error(`Gemini API Error: ${response.status} - ${errorText.substring(0, 200)}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log("Raw Gemini response:", JSON.stringify(data, null, 2));
|
|
|
|
// Extract text from response
|
|
const textContent = data.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
|
|
if (!textContent) {
|
|
console.error("No text content in response:", data);
|
|
throw new Error("No response text from AI");
|
|
}
|
|
|
|
// Parse JSON from the response (remove any markdown formatting if present)
|
|
let resultText = textContent.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
|
|
|
|
// Fix common JSON issues
|
|
resultText = resultText.replace(/'/g, '"'); // Replace single quotes with double quotes
|
|
|
|
console.log("Cleaned response text:", resultText);
|
|
|
|
const result = JSON.parse(resultText);
|
|
console.log("AI Analyze Result:", result);
|
|
res.json(result);
|
|
|
|
} catch (error) {
|
|
console.error("AI Analyze Error:", error);
|
|
res.status(500).json({
|
|
hasFace: false,
|
|
confidence: 0,
|
|
description: `Layanan AI Error: ${error.message}`
|
|
});
|
|
}
|
|
});
|
|
|
|
// POST /api/ai/compare - Compare Faces
|
|
router.post('/compare', async (req, res) => {
|
|
try {
|
|
const { registeredFace, currentImage } = req.body;
|
|
if (!registeredFace || !currentImage) throw new Error("Images missing");
|
|
|
|
const apiKey = getApiKey();
|
|
|
|
// Handle Registered Face (could be URL or Base64)
|
|
let regBase64 = registeredFace;
|
|
let regMimeType = 'image/jpeg';
|
|
|
|
if (registeredFace.startsWith('http')) {
|
|
try {
|
|
console.log("Fetching registered face from URL:", registeredFace);
|
|
const fetchResp = await fetch(registeredFace);
|
|
if (!fetchResp.ok) throw new Error(`Failed to fetch registered face: ${fetchResp.status}`);
|
|
const contentType = fetchResp.headers.get('content-type');
|
|
if (contentType) {
|
|
regMimeType = contentType.split(';')[0];
|
|
}
|
|
const arrayBuffer = await fetchResp.arrayBuffer();
|
|
regBase64 = Buffer.from(arrayBuffer).toString('base64');
|
|
console.log("Successfully fetched and converted to base64");
|
|
} catch (fetchError) {
|
|
console.error("Error fetching registered face:", fetchError);
|
|
throw new Error(`Failed to fetch registered face image: ${fetchError.message}`);
|
|
}
|
|
} else {
|
|
if (registeredFace.startsWith('data:image/png')) {
|
|
regMimeType = 'image/png';
|
|
} else if (registeredFace.startsWith('data:image/webp')) {
|
|
regMimeType = 'image/webp';
|
|
}
|
|
regBase64 = registeredFace.replace(/^data:image\/\w+;base64,/, "");
|
|
}
|
|
|
|
// Handle current image
|
|
let currMimeType = 'image/jpeg';
|
|
if (currentImage.startsWith('data:image/png')) {
|
|
currMimeType = 'image/png';
|
|
} else if (currentImage.startsWith('data:image/webp')) {
|
|
currMimeType = 'image/webp';
|
|
}
|
|
const currBase64 = currentImage.replace(/^data:image\/\w+;base64,/, "");
|
|
|
|
const prompt = `Anda adalah ahli forensik digital. Verifikasi apakah dua foto ini menampilkan orang yang sama untuk absensi sekolah.
|
|
|
|
INSTRUKSI KEAMANAN TINGGI:
|
|
1. Bandingkan fitur biometrik wajah (mata, hidung, mulut, struktur tulang).
|
|
2. PERHATIKAN UMUR: Pastikan Candidate Image adalah foto TERBARU dari orang yang sama.
|
|
3. ANTI-MANIPULASI: Jika Candidate Image terlihat seperti foto hasil crop, foto layar, atau foto kertas, TOLAK dengan tegas.
|
|
4. Pastikan ekspresi wajah wajar (bukan sedang tidur atau dipaksa).
|
|
|
|
Gambar pertama adalah REFERENCE (wajah terdaftar). Gambar kedua adalah CANDIDATE (foto absensi sekarang).
|
|
|
|
PENTING: Berikan respons HANYA dalam format JSON yang valid, tanpa markdown atau backticks:
|
|
{"match": true/false, "confidence": 0-100, "reason": "alasan teknis singkat dalam Bahasa Indonesia"}
|
|
|
|
match harus true jika 95%+ yakin orang yang sama DAN foto asli.`;
|
|
|
|
console.log("Calling Gemini REST API for face comparison...");
|
|
|
|
const requestBody = {
|
|
contents: [{
|
|
parts: [
|
|
{ text: "REFERENCE IMAGE (Wajah Terdaftar):" },
|
|
{
|
|
inline_data: {
|
|
mime_type: regMimeType,
|
|
data: regBase64
|
|
}
|
|
},
|
|
{ text: "CANDIDATE IMAGE (Foto Absensi Sekarang):" },
|
|
{
|
|
inline_data: {
|
|
mime_type: currMimeType,
|
|
data: currBase64
|
|
}
|
|
},
|
|
{ text: prompt }
|
|
]
|
|
}]
|
|
};
|
|
|
|
const response = await fetch(`${GEMINI_API_URL}?key=${apiKey}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(requestBody)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error("Gemini API HTTP Error:", response.status, errorText);
|
|
throw new Error(`Gemini API Error: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log("Raw Gemini response:", JSON.stringify(data, null, 2));
|
|
|
|
const textContent = data.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
|
|
if (!textContent) {
|
|
console.error("No text content in response:", data);
|
|
throw new Error("No comparison response text");
|
|
}
|
|
|
|
let resultText = textContent.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
|
|
resultText = resultText.replace(/'/g, '"');
|
|
|
|
const result = JSON.parse(resultText);
|
|
console.log("AI Compare Result:", result);
|
|
res.json(result);
|
|
|
|
} catch (error) {
|
|
console.error("AI Compare Error:", error);
|
|
res.status(500).json({
|
|
match: false,
|
|
confidence: 0,
|
|
reason: `Server AI Verify Error: ${error.message}`
|
|
});
|
|
}
|
|
});
|
|
|
|
export default router;
|