Files
boc/aamos-api-v1/api/v1/verify.mjs
T

118 lines
4.8 KiB
JavaScript
Raw Normal View History

/**
* 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;