cameraSession integration
This commit is contained in:
@@ -10,8 +10,6 @@ const execPath = '../../../../bin';
|
||||
const binaryPath = path.join(__dirname, execPath, 'extractEmbeddings')
|
||||
|
||||
async function extractEmbedding(base64: string): Promise<string> {
|
||||
console.log(base64);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn(binaryPath, [], {
|
||||
cwd: path.join(__dirname, execPath)
|
||||
@@ -22,9 +20,7 @@ async function extractEmbedding(base64: string): Promise<string> {
|
||||
proc.stdout.on('data', (d) => stdout += d)
|
||||
proc.stderr.on('data', (d) => stderr += d)
|
||||
|
||||
proc.stdin.on('error', (err) => {
|
||||
// ignore EPIPE here, we'll catch it in close handler
|
||||
})
|
||||
proc.stdin.on('error', (err) => { });
|
||||
|
||||
proc.on('error', (err) => {
|
||||
reject(new Error(`Failed to start binary: ${err.message}`))
|
||||
|
||||
@@ -23,6 +23,7 @@ interface Config {
|
||||
transitionCleanupInterval: number;
|
||||
lossThreshold: number;
|
||||
userTTL: number;
|
||||
verifyTimeout: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,7 +51,8 @@ const config: Config = {
|
||||
transitionTTL: 5 * 60 * 1000,
|
||||
transitionCleanupInterval: 60 * 1000,
|
||||
lossThreshold: 5000,
|
||||
userTTL: 3 * 60 * 1000
|
||||
userTTL: 3 * 60 * 1000,
|
||||
verifyTimeout: 30 * 1000
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function updateRoomOccupancyListener(message: string, channel: stri
|
||||
const redis = getRedis();
|
||||
|
||||
if (!lastRoomID) {
|
||||
console.log("[!] Key not found in local mapι")
|
||||
console.log("[!] Key not found in local map")
|
||||
} else {
|
||||
await redis.decr(`room:${lastRoomID}`);
|
||||
}
|
||||
@@ -103,8 +103,6 @@ export async function analyzeData(roomID: string, metrics: {
|
||||
const user = newUser();
|
||||
console.log(`[Redis] Created USER: ${user.psuedoName} USER_ID: ${user.pseudoID}`);
|
||||
|
||||
// trigger camera
|
||||
|
||||
await redis.set(`user:${userID}`, JSON.stringify({
|
||||
userID: user.pseudoID,
|
||||
name: user.psuedoName,
|
||||
@@ -141,8 +139,6 @@ export async function analyzeData(roomID: string, metrics: {
|
||||
|
||||
let transition = transitions.get(userID);
|
||||
|
||||
// update room occupancy
|
||||
|
||||
if (!transition) {
|
||||
console.log("[!] Starting new transition");
|
||||
transition = new RoomTransition();
|
||||
@@ -150,8 +146,6 @@ export async function analyzeData(roomID: string, metrics: {
|
||||
}
|
||||
|
||||
if (transition.shouldTransitionTo(roomID, metrics.rssi, resObj["rssi"], resObj["timestamp"])) {
|
||||
// trigger camera
|
||||
|
||||
console.log(`[!] Transition done ${resObj["room"]} -> ${roomID}`);
|
||||
|
||||
await redis.decr(`room:${resObj["room"]}`);
|
||||
@@ -162,7 +156,7 @@ export async function analyzeData(roomID: string, metrics: {
|
||||
resObj["rssi"] = metrics.rssi;
|
||||
resObj["room"] = roomID;
|
||||
resObj["timestamp"] = Date.now()
|
||||
|
||||
resObj["verified"] = false;
|
||||
|
||||
await redis.set(`user:${userID}`, JSON.stringify(resObj), { PX: config.core.userTTL });
|
||||
} else
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getRoomIDs } from "../api/livedata/livedata.repository.js";
|
||||
import { initRedis, initSubRedis } from "../redis/redis.js";
|
||||
import { initCamera } from "./camera.js";
|
||||
import * as mqtt from "./mqtt.js";
|
||||
import { cleanupWorker } from "./transition.js";
|
||||
|
||||
@@ -19,6 +20,8 @@ export async function bootstrap() {
|
||||
|
||||
cleanupWorker();
|
||||
|
||||
await initCamera();
|
||||
|
||||
console.log("[!] Core started.");
|
||||
} catch (err: any) {
|
||||
console.log("[!] Bootstrap failed:", err.message);
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { fileURLToPath } from 'url'
|
||||
import path from 'path'
|
||||
import { spawn } from "child_process";
|
||||
import { getRedis } from '../redis/redis.js';
|
||||
import { prisma } from '../lib/prisma.js';
|
||||
import config from '../config/config.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const execPath = '../../bin';
|
||||
const binaryPath = path.join(__dirname, execPath, 'cameraSession')
|
||||
|
||||
type InputPacket = {
|
||||
uuid: string
|
||||
descriptor: number[]
|
||||
}
|
||||
|
||||
type ResultPacket = {
|
||||
uuid: string
|
||||
verified: boolean
|
||||
}
|
||||
|
||||
export async function getUnverifiedUserKeys(): Promise<string[]> {
|
||||
const redis = getRedis()
|
||||
|
||||
const keys = await redis.keys('user:*')
|
||||
|
||||
const unverified: string[] = []
|
||||
|
||||
await Promise.all(keys.map(async (key) => {
|
||||
const raw = await redis.get(key)
|
||||
if (!raw) return
|
||||
const session = JSON.parse(raw)
|
||||
if (!session.verified) unverified.push(key.replace('user:', ''))
|
||||
}))
|
||||
|
||||
return unverified
|
||||
}
|
||||
|
||||
export async function getUnverifiedUsersWithEmbeddings(): Promise<{ uuid: string, descriptor: number[] }[]> {
|
||||
const userIds = await getUnverifiedUserKeys()
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
id: { in: userIds },
|
||||
faceEmbedding: { not: null },
|
||||
},
|
||||
select: { id: true, faceEmbedding: true },
|
||||
})
|
||||
|
||||
return users.map((user) => ({
|
||||
uuid: user.id,
|
||||
descriptor: JSON.parse(user.faceEmbedding!),
|
||||
}))
|
||||
}
|
||||
|
||||
async function cameraSession(packets: InputPacket[]): Promise<ResultPacket[] | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn(binaryPath, [], {
|
||||
cwd: path.join(__dirname, execPath)
|
||||
})
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
|
||||
proc.stdout.on('data', (d) => stdout += d)
|
||||
proc.stderr.on('data', (d) => stderr += d)
|
||||
proc.stdin.on('error', () => { })
|
||||
|
||||
proc.on('error', (err) => {
|
||||
reject(new Error(`Failed to start binary: ${err.message}`))
|
||||
})
|
||||
|
||||
proc.on('close', (code) => {
|
||||
console.log('cameraSession exited with code', code)
|
||||
const trimmed = stdout.trim()
|
||||
if (trimmed === 'no face found.') return resolve(null)
|
||||
if (code !== 0) return reject(new Error(stderr || `exited with code ${code}`))
|
||||
try {
|
||||
const results: ResultPacket[] = JSON.parse(trimmed)
|
||||
resolve(results)
|
||||
} catch (e) {
|
||||
reject(new Error(`Failed to parse output: ${trimmed}`))
|
||||
}
|
||||
})
|
||||
|
||||
for (const packet of packets) {
|
||||
proc.stdin.write(JSON.stringify(packet) + '\n')
|
||||
}
|
||||
proc.stdin.end()
|
||||
})
|
||||
}
|
||||
|
||||
const setUnverified = async (uuid: string) => {
|
||||
const redis = getRedis()
|
||||
const key = `user:${uuid}`
|
||||
const raw = await redis.get(key)
|
||||
if (!raw) return
|
||||
const session = JSON.parse(raw)
|
||||
session.verified = false
|
||||
await redis.set(key, JSON.stringify(session))
|
||||
console.log(`[Camera] Unverified user ${uuid} due to timeout`)
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
const redis = getRedis();
|
||||
|
||||
try {
|
||||
const packets = await getUnverifiedUsersWithEmbeddings()
|
||||
if (packets.length > 0) {
|
||||
const results = await cameraSession(packets)
|
||||
if (results) {
|
||||
await Promise.all(results.map(async (r) => {
|
||||
if (!r.verified) return
|
||||
const key = `user:${r.uuid}`
|
||||
const raw = await redis.get(key)
|
||||
if (!raw) return
|
||||
const session = JSON.parse(raw)
|
||||
session.verified = true
|
||||
await redis.set(key, JSON.stringify(session))
|
||||
|
||||
setTimeout(() => setUnverified(r.uuid), config.core.verifyTimeout)
|
||||
|
||||
console.log(`[Camera] Verified user ${r.uuid}`)
|
||||
}))
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('camera run error:', e)
|
||||
}
|
||||
setTimeout(run, 1000)
|
||||
}
|
||||
|
||||
export { run as initCamera };
|
||||
Reference in New Issue
Block a user