/** * POST /v1/track — Spårning över tid * Sparar tracking-data och analyserar förändringar över tid */ import { Router } from 'express'; import * as ort from 'onnxruntime-node'; import sharp from 'sharp'; import { fetchImage, hashInput, saveResult, genReqId, requireAuth, dbPool } from './utils.mjs'; const router = Router(); const FACE_DET_MODEL = '/opt/amos/data/kyc-service/models/face_detection_yunet_2023mar.onnx'; const FACE_REC_MODEL = '/opt/amos/data/kyc-service/models/face_recognition_sface_2021dec.onnx'; let detSession = null, recSession = null; async function getDetSession() { if (!detSession) detSession = await ort.InferenceSession.create(FACE_DET_MODEL); return detSession; } async function getRecSession() { if (!recSession) recSession = await ort.InferenceSession.create(FACE_REC_MODEL); return recSession; } async function getFaceEmbedding(buffer) { // Detect face const raw = await sharp(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 tensor = new ort.Tensor('float32', floatData, [1, 3, h, w]); const detSess = await getDetSession(); const feeds = {}; feeds[detSess.inputNames[0]] = tensor; const out = await detSess.run(feeds); const outTensor = out[detSess.outputNames[0]]; const outData = outTensor.data; const dims = outTensor.dims; const stride = dims[dims.length - 1]; 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; } } if (!bestBox || bestScore < 0.5) return null; // Get embedding const orig = await sharp(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(buffer) .extract({ left: x1, top: y1, width: x2 - x1, height: y2 - y1 }) .resize(112, 112) .raw() .toBuffer(); const recFloat = new Float32Array(1 * 3 * 112 * 112); for (let i = 0; i < 112 * 112; i++) { recFloat[0 * 12544 + i] = faceBuf[i * 3] / 255.0; recFloat[1 * 12544 + i] = faceBuf[i * 3 + 1] / 255.0; recFloat[2 * 12544 + i] = faceBuf[i * 3 + 2] / 255.0; } const recTensor = new ort.Tensor('float32', recFloat, [1, 3, 112, 112]); const recSess = await getRecSession(); const recFeeds = {}; recFeeds[recSess.inputNames[0]] = recTensor; const recOut = await recSess.run(recFeeds); return { embedding: Array.from(recOut[recSess.outputNames[0]].data), face_confidence: parseFloat(bestScore.toFixed(4)), bbox: { x: x1, y: y1, width: x2 - x1, height: y2 - y1 } }; } function cosineSimilarity(a, b) { let dot = 0, na = 0, nb = 0; for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i]; } return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-6); } router.post('/', requireAuth, async (req, res) => { const requestId = genReqId(); const start = Date.now(); try { const { image_url, image_base64, track_id, track_type = 'face' } = req.body || {}; if (!track_id) return res.status(400).json({ ok: false, error: 'track_id required' }); const img = await fetchImage({ image_url, image_base64 }); const inputHash = hashInput(img.buffer); // Get face embedding for tracking const faceData = await getFaceEmbedding(img.buffer); if (!faceData) { return res.status(400).json({ ok: false, error: 'No face detected for tracking', track_id }); } // Save tracking event await dbPool.query( `CREATE TABLE IF NOT EXISTS aamos_tracking ( id SERIAL PRIMARY KEY, track_id VARCHAR(128) NOT NULL, track_type VARCHAR(32) NOT NULL, request_id VARCHAR(64) NOT NULL, embedding VECTOR(512), face_confidence NUMERIC(5,4), bbox JSONB, input_hash VARCHAR(64), created_at TIMESTAMPTZ DEFAULT NOW() )` ).catch(() => {}); // Ignore if exists or pgvector not available // Try to insert without vector type first try { await dbPool.query( `INSERT INTO aamos_tracking (track_id, track_type, request_id, embedding, face_confidence, bbox, input_hash) VALUES ($1,$2,$3,$4,$5,$6,$7)`, [track_id, track_type, requestId, JSON.stringify(faceData.embedding), faceData.face_confidence, JSON.stringify(faceData.bbox), inputHash] ); } catch (dbErr) { // Fallback: create simple table without vector await dbPool.query( `CREATE TABLE IF NOT EXISTS aamos_tracking_simple ( id SERIAL PRIMARY KEY, track_id VARCHAR(128) NOT NULL, track_type VARCHAR(32) NOT NULL, request_id VARCHAR(64) NOT NULL, face_confidence NUMERIC(5,4), bbox JSONB, input_hash VARCHAR(64), created_at TIMESTAMPTZ DEFAULT NOW() )` ); await dbPool.query( `INSERT INTO aamos_tracking_simple (track_id, track_type, request_id, face_confidence, bbox, input_hash) VALUES ($1,$2,$3,$4,$5,$6)`, [track_id, track_type, requestId, faceData.face_confidence, JSON.stringify(faceData.bbox), inputHash] ); } // Find previous tracking events for this track_id let previousEvents = []; try { const { rows } = await dbPool.query( `SELECT request_id, face_confidence, bbox, created_at FROM aamos_tracking_simple WHERE track_id=$1 AND request_id!=$2 ORDER BY created_at DESC LIMIT 5`, [track_id, requestId] ); previousEvents = rows; } catch {} // Calculate similarity with previous if available let similarity = null; if (previousEvents.length > 0 && previousEvents[0].embedding) { try { const prevEmb = JSON.parse(previousEvents[0].embedding); similarity = parseFloat(cosineSimilarity(faceData.embedding, prevEmb).toFixed(4)); } catch {} } const confidence = faceData.face_confidence; const result = { ok: true, endpoint: 'track', request_id: requestId, track_id, track_type, face_detected: true, face_confidence: faceData.face_confidence, bbox: faceData.bbox, previous_sightings: previousEvents.length, similarity_to_previous: similarity, tracking_status: similarity !== null ? (similarity > 0.7 ? 'confirmed_match' : 'possible_match') : 'new_tracking', inference_time_ms: Date.now() - start, }; await saveResult('track', requestId, inputHash, result, confidence, { track_id, track_type, source: img.source }); res.json(result); } catch (e) { console.error('[track]', e); res.status(500).json({ ok: false, error: e.message, request_id: requestId }); } }); export default router;