You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
123 lines
4.5 KiB
123 lines
4.5 KiB
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)); |
|
}
|
|
|