docs: add quixzoom-auth-core product to AAMOS
- Product documentation in docs/products/ - Updated MEMORY.md with product info - quiXzoom Auth Core as AAMOS Identity product
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user