Initial commit: digital reception monorepo (M1-M11 + demo extensions)
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import { PrismaClient } from '@reception/db';
|
||||
import { ApiClient, FaceServiceClient } from './clients.js';
|
||||
import { makeEmbedding, embeddingFingerprint } from './embedding.js';
|
||||
import type { Scenario } from './types.js';
|
||||
|
||||
export interface RunOptions {
|
||||
scenario: Scenario;
|
||||
apiBaseUrl: string;
|
||||
faceServiceUrl: string;
|
||||
prisma: PrismaClient;
|
||||
realtime: boolean;
|
||||
}
|
||||
|
||||
export interface RunResult {
|
||||
trackIds: string[];
|
||||
embeddingsSaved: number;
|
||||
eventsSent: number;
|
||||
visitCreated?: { visitId: string; patientId: string; confidence: number } | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Прогоняет сценарий: создаёт треки (по одному на камеру в сценарии),
|
||||
* сохраняет эмбеддинги, шлёт события. Если scenario.triggerRecognition —
|
||||
* после всех событий пытается узнать persona и создать Visit.
|
||||
*/
|
||||
export async function runScenario(opts: RunOptions): Promise<RunResult> {
|
||||
const { scenario, apiBaseUrl, faceServiceUrl, prisma } = opts;
|
||||
const api = new ApiClient(apiBaseUrl);
|
||||
const face = new FaceServiceClient(faceServiceUrl);
|
||||
|
||||
const baseTime = new Date();
|
||||
const personaEmbedding = makeEmbedding(scenario.personaSeed, 0);
|
||||
console.log(
|
||||
`[scenario:${scenario.name}] persona seed=${scenario.personaSeed}, fp=${embeddingFingerprint(personaEmbedding)}`,
|
||||
);
|
||||
|
||||
const tracksByCamera = new Map<string, { trackId: string; cameraId: string; zoneCode: string }>();
|
||||
const result: RunResult = { trackIds: [], embeddingsSaved: 0, eventsSent: 0, visitCreated: null };
|
||||
|
||||
for (const event of scenario.events) {
|
||||
const occurredAt = new Date(baseTime.getTime() + event.offsetSec * 1000);
|
||||
|
||||
let track = tracksByCamera.get(event.cameraName);
|
||||
if (!track) {
|
||||
const created = await api.createTrack(event.cameraName, occurredAt);
|
||||
track = { trackId: created.trackId, cameraId: created.cameraId, zoneCode: event.zoneCode };
|
||||
tracksByCamera.set(event.cameraName, track);
|
||||
result.trackIds.push(track.trackId);
|
||||
|
||||
// Сохраняем N эмбеддингов с малым jitter — чтобы было что искать.
|
||||
for (let i = 0; i < scenario.embeddingsPerCamera; i++) {
|
||||
const jittered = makeEmbedding(scenario.personaSeed, 0.0001 * i);
|
||||
await face.saveRawEmbedding({
|
||||
embedding: jittered,
|
||||
trackId: track.trackId,
|
||||
cameraId: track.cameraId,
|
||||
capturedAt: new Date(occurredAt.getTime() + i * 100),
|
||||
});
|
||||
result.embeddingsSaved += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.realtime && event.offsetSec > 0) {
|
||||
await sleep(Math.min(event.offsetSec * 1000, 5000));
|
||||
}
|
||||
|
||||
await api.addEvent({
|
||||
trackId: track.trackId,
|
||||
type: event.type,
|
||||
cameraName: event.cameraName,
|
||||
zoneCode: event.zoneCode,
|
||||
occurredAt,
|
||||
});
|
||||
result.eventsSent += 1;
|
||||
console.log(
|
||||
`[scenario:${scenario.name}] event ${event.type} on ${event.cameraName} at +${event.offsetSec}s`,
|
||||
);
|
||||
}
|
||||
|
||||
if (scenario.triggerRecognition) {
|
||||
const match = await face.recognizeByEmbedding(personaEmbedding);
|
||||
if (match) {
|
||||
console.log(
|
||||
`[scenario:${scenario.name}] recognized patient ${match.patient_id} (confidence=${match.confidence})`,
|
||||
);
|
||||
|
||||
// Если уже есть Visit для этого пациента в окне ~5 мин — переиспользуем (idempotent).
|
||||
const recentVisit = await prisma.visit.findFirst({
|
||||
where: {
|
||||
patientId: match.patient_id,
|
||||
arrivedAt: { gte: new Date(Date.now() - 5 * 60 * 1000) },
|
||||
},
|
||||
});
|
||||
if (recentVisit) {
|
||||
result.visitCreated = {
|
||||
visitId: recentVisit.id,
|
||||
patientId: match.patient_id,
|
||||
confidence: match.confidence,
|
||||
};
|
||||
} else {
|
||||
const visit = await prisma.visit.create({
|
||||
data: { patientId: match.patient_id, arrivedAt: baseTime },
|
||||
});
|
||||
result.visitCreated = {
|
||||
visitId: visit.id,
|
||||
patientId: match.patient_id,
|
||||
confidence: match.confidence,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
console.log(`[scenario:${scenario.name}] persona не узнан (нет enrolled-эмбеддинга)`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[scenario:${scenario.name}] DONE — tracks=${result.trackIds.length}, embeddings=${result.embeddingsSaved}, events=${result.eventsSent}, visit=${result.visitCreated ? 'created' : 'none'}`,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
Reference in New Issue
Block a user