/** * POST /v1/verify — Identitetskontroll (liveness, dokument) * Använder befintlig MiniFASNetV2 (anti-spoofing/liveness) + YuNet face detection */ import { Router } from 'express'; import * as ort from 'onnxruntime-node'; import sharp from 'sharp'; import { fetchImage, hashInput, saveResult, genReqId, requireAuth } from './utils.mjs'; const router = Router(); const FACE_DET_MODEL = '/opt/amos/data/kyc-service/models/face_detection_yunet_2023mar.onnx'; const LIVENESS_MODEL = '/opt/amos/data/kyc-service/models/2.7_80x80_MiniFASNetV2.onnx'; let detSession = null, liveSession = null; async function getDetSession() { if (!detSession) detSession = await ort.InferenceSession.create(FACE_DET_MODEL); return detSession; } async function getLiveSession() { if (!liveSession) liveSession = await ort.InferenceSession.create(LIVENESS_MODEL); return liveSession; } router.post('/', requireAuth, async (req, res) => { const requestId = genReqId(); const start = Date.now(); try { const { image_url, image_base64, check_type = 'liveness' } = req.body || {}; const img = await fetchImage({ image_url, image_base64 }); const inputHash = hashInput(img.buffer); // Step 1: Detect face const raw = await sharp(img.buffer).resize(640, 640).raw().toBuffer({ resolveWithObject: true }); const { data, info } = raw; const h = info.height, w = info.width; const floatData = new Float32Array(1 * 3 * h * w); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { const idx = (y * w + x) * 3; floatData[0 * h * w + y * w + x] = data[idx] / 255.0; floatData[1 * h * w + y * w + x] = data[idx + 1] / 255.0; floatData[2 * h * w + y * w + x] = data[idx + 2] / 255.0; } } const detTensor = new ort.Tensor('float32', floatData, [1, 3, h, w]); const detSess = await getDetSession(); const detFeeds = {}; detFeeds[detSess.inputNames[0]] = detTensor; const detOut = await detSess.run(detFeeds); const outTensor = detOut[detSess.outputNames[0]]; const outData = outTensor.data; const dims = outTensor.dims; const stride = dims[dims.length - 1]; let faceFound = false; let bestScore = 0, bestBox = null; for (let i = 0; i < dims[0]; i++) { const row = Array.from(outData.slice(i * stride, (i + 1) * stride)); if (row[2] > bestScore) { bestScore = row[2]; bestBox = row; } } faceFound = bestScore > 0.5; // Step 2: Liveness check (anti-spoofing) let livenessScore = null, livenessLabel = 'unknown'; if (faceFound && check_type === 'liveness') { // Crop face region and resize to 80x80 for MiniFASNet const orig = await sharp(img.buffer).raw().toBuffer({ resolveWithObject: true }); const ow = orig.info.width, oh = orig.info.height; const x1 = Math.max(0, Math.round(bestBox[3] * ow)); const y1 = Math.max(0, Math.round(bestBox[4] * oh)); const x2 = Math.min(ow, Math.round(bestBox[5] * ow)); const y2 = Math.min(oh, Math.round(bestBox[6] * oh)); const faceBuf = await sharp(img.buffer) .extract({ left: x1, top: y1, width: x2 - x1, height: y2 - y1 }) .resize(80, 80) .raw() .toBuffer(); const liveFloat = new Float32Array(1 * 3 * 80 * 80); for (let i = 0; i < 80 * 80; i++) { liveFloat[0 * 6400 + i] = faceBuf[i * 3] / 255.0; liveFloat[1 * 6400 + i] = faceBuf[i * 3 + 1] / 255.0; liveFloat[2 * 6400 + i] = faceBuf[i * 3 + 2] / 255.0; } const liveTensor = new ort.Tensor('float32', liveFloat, [1, 3, 80, 80]); const liveSess = await getLiveSession(); const liveFeeds = {}; liveFeeds[liveSess.inputNames[0]] = liveTensor; const liveOut = await liveSess.run(liveFeeds); const liveData = liveOut[liveSess.outputNames[0]].data; // MiniFASNetV2 output: [real_score, fake_score] const realScore = liveData[0]; const fakeScore = liveData[1]; livenessScore = parseFloat((realScore / (realScore + fakeScore + 1e-6)).toFixed(4)); livenessLabel = livenessScore > 0.7 ? 'live' : livenessScore > 0.4 ? 'uncertain' : 'spoof'; } const confidence = faceFound ? (livenessScore ?? bestScore) : 0; const result = { ok: true, endpoint: 'verify', request_id: requestId, check_type, face_detected: faceFound, face_confidence: parseFloat(bestScore.toFixed(4)), liveness: { score: livenessScore, label: livenessLabel }, verified: faceFound && livenessLabel === 'live', inference_time_ms: Date.now() - start, }; await saveResult('verify', requestId, inputHash, result, confidence, { check_type, source: img.source }); res.json(result); } catch (e) { console.error('[verify]', e); res.status(500).json({ ok: false, error: e.message, request_id: requestId }); } }); export default router;