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 { 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(); 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 { return new Promise((r) => setTimeout(r, ms)); }