From 44310212415e32bb05d5bb86a60b81b6b661f695 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 28 May 2024 23:11:45 +0530 Subject: [PATCH 1/7] dump vitals from observation and image to s3 while creating automated daily round --- src/cron/automatedDailyRounds.ts | 55 ++++++++++++++++++++++++++------ src/utils/configs.ts | 6 ++++ 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/cron/automatedDailyRounds.ts b/src/cron/automatedDailyRounds.ts index e079211..47adc3a 100644 --- a/src/cron/automatedDailyRounds.ts +++ b/src/cron/automatedDailyRounds.ts @@ -1,4 +1,5 @@ import axios, { AxiosError, AxiosResponse } from "axios"; +import { randomUUID } from "crypto"; import fs from "fs"; import path from "path"; @@ -9,17 +10,27 @@ import prisma from "@/lib/prisma"; import { AssetBed } from "@/types/asset"; import { CameraParams } from "@/types/camera"; import { CarePaginatedResponse } from "@/types/care"; -import { DailyRoundObservation, Observation, ObservationType } from "@/types/observation"; +import { + DailyRoundObservation, + Observation, + ObservationType, +} from "@/types/observation"; import { OCRV2Response } from "@/types/ocr"; import { CameraUtils } from "@/utils/CameraUtils"; import { isValid } from "@/utils/ObservationUtils"; import { generateHeaders } from "@/utils/assetUtils"; -import { careApi, openaiApiKey, saveDailyRound } from "@/utils/configs"; +import { + careApi, + hostname, + openaiApiKey, + s3SaveDailyRound, + saveDailyRound, +} from "@/utils/configs"; import { getPatientId } from "@/utils/dailyRoundUtils"; import { downloadImage } from "@/utils/downloadImageWithDigestRouter"; +import { makeDataDumpToJson } from "@/utils/makeDataDump"; import { parseVitalsFromImage } from "@/utils/ocr"; - const UPDATE_INTERVAL = 60 * 60 * 1000; export async function getMonitorPreset(bedId: string, assetId: string) { @@ -64,8 +75,8 @@ export async function getMonitorPreset(bedId: string, assetId: string) { export async function saveImageLocally( snapshotUrl: string, camParams: CameraParams, + fileName = `image--${new Date().getTime()}.jpeg`, ) { - const fileName = `image--${new Date().getTime()}.jpeg`; const imagePath = path.resolve("images", fileName); await downloadImage( snapshotUrl, @@ -92,7 +103,7 @@ export async function getVitalsFromImage(imageUrl: string) { // ? date.toISOString() // : new Date().toISOString(); const isoDate = new Date().toISOString(); - + const payload = { taken_at: isoDate, spo2: data.spO2?.oxygen_saturation_percentage ?? null, @@ -281,11 +292,16 @@ export async function automatedDailyRounds() { return; } - let vitals: DailyRoundObservation | null = await getVitalsFromObservations( - monitor.ipAddress, - ); + const _id = randomUUID(); + let vitals: DailyRoundObservation | null = s3SaveDailyRound + ? null + : await getVitalsFromObservations(monitor.ipAddress); - console.log(`Vitals from observations: ${JSON.stringify(vitals)}`); + console.log( + saveDailyRound + ? "Skipping vitals from observations as saving daily round is enabled" + : `Vitals from observations: ${JSON.stringify(vitals)}`, + ); if (!vitals && openaiApiKey) { if (!asset_beds || asset_beds.length === 0) { @@ -325,7 +341,7 @@ export async function automatedDailyRounds() { const snapshotUrl = await CameraUtils.getSnapshotUri({ camParams: camera, }); - const imageUrl = await saveImageLocally(snapshotUrl.uri, camera); + const imageUrl = await saveImageLocally(snapshotUrl.uri, camera, _id); CameraUtils.unlockCamera(camera.hostname); @@ -333,6 +349,25 @@ export async function automatedDailyRounds() { console.log(`Vitals from image: ${JSON.stringify(vitals)}`); } + if (s3SaveDailyRound) { + const vitalsFromObservation = await getVitalsFromObservations( + monitor.ipAddress, + ); + console.log( + `Vitals from observations: ${JSON.stringify(vitalsFromObservation)}`, + ); + + makeDataDumpToJson( + { + vitalsFromObservation, + vitalsFromImage: vitals, + }, + `${hostname}/daily-rounds/${_id}.json`, + ); + + vitals = vitalsFromObservation ?? vitals; + } + if (!vitals || !payloadHasData(vitals)) { console.error(`No vitals found for the patient ${patient_id}`); return; diff --git a/src/utils/configs.ts b/src/utils/configs.ts index 0a5e14f..b932d50 100644 --- a/src/utils/configs.ts +++ b/src/utils/configs.ts @@ -33,4 +33,10 @@ export const s3BucketName = process.env.S3_BUCKET_NAME; export const s3AccessKeyId = process.env.S3_ACCESS_KEY_ID; export const s3SecretAccessKey = process.env.S3_SECRET_ACCESS_KEY; +export const s3SaveDailyRound = + Boolean(process.env.S3_SAVE_DAILY_ROUND) && + s3BucketName && + s3AccessKeyId && + s3SecretAccessKey; + export const openaiApiKey = process.env.OPENAI_API_KEY ?? ""; \ No newline at end of file From 1d5e3239e25a91e7b5d405e2f0c1ba38df529e33 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 29 May 2024 20:40:41 +0530 Subject: [PATCH 2/7] add support for using azure openai --- package-lock.json | 14 +++---- package.json | 4 +- src/utils/configs.ts | 7 +++- src/utils/ocr.ts | 88 ++++++++++++++++++++++++++------------------ 4 files changed, 68 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index 81e9881..2edf63d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,7 @@ "morgan": "^1.10.0", "node-cron": "^3.0.3", "onvif": "^0.6.5", - "openai": "^4.33.1", + "openai": "^4.47.2", "pidusage": "^3.0.0", "sharp": "^0.33.3", "swagger-jsdoc": "^6.1.0", @@ -3338,9 +3338,9 @@ } }, "node_modules/openai": { - "version": "4.33.1", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.33.1.tgz", - "integrity": "sha512-0DH572aSxGTT1JPOXgJQ9mjiuSPg/7scPot8hLc5I1mfQxPxLXTZWJpWerKaIWOuPkR2nrB0SamGDEehH8RuWA==", + "version": "4.47.2", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.47.2.tgz", + "integrity": "sha512-E3Wq9mYdDSLajmcJm9RO/lCegTKrQ7ilAkMbhob4UgGhTjHwIHI+mXNDNPl5+sGIUp2iVUkpoi772FjYa7JlqA==", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", @@ -6697,9 +6697,9 @@ } }, "openai": { - "version": "4.33.1", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.33.1.tgz", - "integrity": "sha512-0DH572aSxGTT1JPOXgJQ9mjiuSPg/7scPot8hLc5I1mfQxPxLXTZWJpWerKaIWOuPkR2nrB0SamGDEehH8RuWA==", + "version": "4.47.2", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.47.2.tgz", + "integrity": "sha512-E3Wq9mYdDSLajmcJm9RO/lCegTKrQ7ilAkMbhob4UgGhTjHwIHI+mXNDNPl5+sGIUp2iVUkpoi772FjYa7JlqA==", "requires": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", diff --git a/package.json b/package.json index 2555ccf..cc5b1ef 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "morgan": "^1.10.0", "node-cron": "^3.0.3", "onvif": "^0.6.5", - "openai": "^4.33.1", + "openai": "^4.47.2", "pidusage": "^3.0.0", "sharp": "^0.33.3", "swagger-jsdoc": "^6.1.0", @@ -86,4 +86,4 @@ "engines": { "node": ">=20.0.0" } -} +} \ No newline at end of file diff --git a/src/utils/configs.ts b/src/utils/configs.ts index b932d50..99a9e36 100644 --- a/src/utils/configs.ts +++ b/src/utils/configs.ts @@ -39,4 +39,9 @@ export const s3SaveDailyRound = s3AccessKeyId && s3SecretAccessKey; -export const openaiApiKey = process.env.OPENAI_API_KEY ?? ""; \ No newline at end of file +export const openaiApiKey = process.env.OPENAI_API_KEY ?? ""; +export const openaiEndpoint = process.env.OPENAI_ENDPOINT ?? ""; +export const openaiApiVersion = process.env.OPENAI_API_VERSION ?? "2024-02-01"; +export const openaiVisionModel = + process.env.OPENAI_VISION_MODEL ?? "vision-preview"; +export const openaiUseAzure = openaiEndpoint.includes("azure.com"); \ No newline at end of file diff --git a/src/utils/ocr.ts b/src/utils/ocr.ts index f2a6f9f..97c7ebd 100644 --- a/src/utils/ocr.ts +++ b/src/utils/ocr.ts @@ -1,11 +1,22 @@ -import { openaiApiKey } from "./configs"; -import OpenAI from "openai"; +import { + openaiApiKey, + openaiApiVersion, + openaiEndpoint, + openaiUseAzure, + openaiVisionModel, +} from "./configs"; +import { AzureOpenAI, OpenAI } from "openai"; import sharp from "sharp"; - -const openai = new OpenAI({ - apiKey: openaiApiKey, -}); +const openai = openaiUseAzure + ? new AzureOpenAI({ + apiKey: openaiApiKey, + endpoint: openaiEndpoint, + apiVersion: openaiApiVersion, + }) + : new OpenAI({ + apiKey: openaiApiKey, + }); export async function compressImage(image: Buffer) { return await sharp(image).resize(1000).jpeg({ quality: 80 }).toBuffer(); @@ -20,15 +31,16 @@ export async function parseVitalsFromImage(image: Buffer) { const b64Image = encodeImage(compressedImage); const imageUrl = `data:image/jpeg;base64,${b64Image}`; - const completions = await openai.chat.completions.create({ - model: "gpt-4-turbo", - max_tokens: 4096, - temperature: 0.4, - response_format: { type: "json_object" }, - messages: [ - { - role: "system", - content: ` + try { + const completions = await openai.chat.completions.create({ + model: openaiVisionModel, + max_tokens: 4096, + temperature: 0.4, + // response_format: { type: "json_object" }, + messages: [ + { + role: "system", + content: ` You are an expert 5Para Monitor reader of patients. You are given 5Para Monitor image, analyze it and predict patient's reading, you will output the readings in minified JSON format only. Tips to analyze the ocr data: monitor can be zoomed in or zoomed out, most of the times readings that we want are at extreme right of the monitor screen, use expertise in reading 5ParaMonitor to make educated guesses about the correct reading of a field. @@ -37,29 +49,35 @@ export async function parseVitalsFromImage(image: Buffer) { Example output in minified JSON format: {"time_stamp":"yyyy-mm-ddThh:mm:ss","ecg":{"Heart_Rate_bpm":},"nibp":{"systolic_mmhg":,"diastolic_mmhg":,"mean_arterial_pressure_mmhg":},"spO2":{"oxygen_saturation_percentage":},"respiration_rate":{"breaths_per_minute":},"temperature":{"fahrenheit":}} + + The output should be minified JSON format only. `.trim(), - }, - { - role: "user", - content: [ - { - type: "image_url", - image_url: { - url: imageUrl, + }, + { + role: "user", + content: [ + { + type: "image_url", + image_url: { + url: imageUrl, + }, }, - }, - ], - }, - ], - }); + ], + }, + ], + }); + + const response = completions.choices.shift()?.message.content; - const response = completions.choices.shift()?.message.content; + if (!response) { + console.error("Failed to get response from OpenAI"); + return null; + } - if (!response) { - console.error("Failed to get response from OpenAI"); + console.log(`[OCR] : ${response}`); + return JSON.parse(response); + } catch (error) { + console.error("Failed to get response from OpenAI", error); return null; } - - console.log(`[OCR] : ${response}`); - return JSON.parse(response); -} \ No newline at end of file +} From 6725352354b1b64d8d6de4d915de5b5c4f42512c Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 30 May 2024 12:16:29 +0530 Subject: [PATCH 3/7] added vitals stat accuracy route --- .../migration.sql | 14 ++++ prisma/schema.prisma | 12 ++++ src/controller/VitalsStatController.ts | 21 ++++++ src/cron/automatedDailyRounds.ts | 52 +++++++++----- src/cron/vitalsStatS3Dump.ts | 56 +++++++++++++++ src/index.ts | 11 ++- src/router/vitalsStatRouter.ts | 9 +++ src/server.ts | 6 +- src/utils/configs.ts | 12 ++-- src/utils/vitalsAccuracy.ts | 71 +++++++++++++++++++ 10 files changed, 238 insertions(+), 26 deletions(-) create mode 100644 prisma/migrations/20240530063454_add_stats_vital/migration.sql create mode 100644 src/controller/VitalsStatController.ts create mode 100644 src/cron/vitalsStatS3Dump.ts create mode 100644 src/router/vitalsStatRouter.ts create mode 100644 src/utils/vitalsAccuracy.ts diff --git a/prisma/migrations/20240530063454_add_stats_vital/migration.sql b/prisma/migrations/20240530063454_add_stats_vital/migration.sql new file mode 100644 index 0000000..9ac2ba1 --- /dev/null +++ b/prisma/migrations/20240530063454_add_stats_vital/migration.sql @@ -0,0 +1,14 @@ +-- CreateTable +CREATE TABLE "VitalsStat" ( + "id" SERIAL NOT NULL, + "imageId" TEXT NOT NULL, + "vitalsFromObservation" JSONB NOT NULL, + "vitalsFromImage" JSONB NOT NULL, + "gptDetails" JSONB NOT NULL, + "accuracy" DOUBLE PRECISION NOT NULL, + "cumulativeAccuracy" DOUBLE PRECISION NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "VitalsStat_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 78161cb..bf59840 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -65,3 +65,15 @@ model DailyRound { time DateTime @default(now()) asset Asset @relation(fields: [assetExternalId], references: [externalId], onDelete: Cascade, onUpdate: Cascade) } + +model VitalsStat { + id Int @id @default(autoincrement()) + imageId String + vitalsFromObservation Json + vitalsFromImage Json + gptDetails Json + accuracy Float + cumulativeAccuracy Float + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/src/controller/VitalsStatController.ts b/src/controller/VitalsStatController.ts new file mode 100644 index 0000000..9c9faa9 --- /dev/null +++ b/src/controller/VitalsStatController.ts @@ -0,0 +1,21 @@ +import { Request, Response } from "express"; + +import prisma from "@/lib/prisma"; + +export class VitalsStatController { + static latestAccuracy = async (req: Request, res: Response) => { + const vitalsStat = await prisma.vitalsStat.findFirst({ + orderBy: { createdAt: "desc" }, + }); + + if (!vitalsStat) { + return res.status(404).json({ message: "No vitals stat found" }); + } + + return res.status(200).json({ + accuracy: vitalsStat.accuracy, + cumulativeAccuracy: vitalsStat.cumulativeAccuracy, + time: vitalsStat.createdAt, + }); + }; +} diff --git a/src/cron/automatedDailyRounds.ts b/src/cron/automatedDailyRounds.ts index 47adc3a..08fefc0 100644 --- a/src/cron/automatedDailyRounds.ts +++ b/src/cron/automatedDailyRounds.ts @@ -19,17 +19,12 @@ import { OCRV2Response } from "@/types/ocr"; import { CameraUtils } from "@/utils/CameraUtils"; import { isValid } from "@/utils/ObservationUtils"; import { generateHeaders } from "@/utils/assetUtils"; -import { - careApi, - hostname, - openaiApiKey, - s3SaveDailyRound, - saveDailyRound, -} from "@/utils/configs"; +import { careApi, openaiApiKey, openaiApiVersion, openaiVisionModel, saveDailyRound, saveVitalsStat } from "@/utils/configs"; import { getPatientId } from "@/utils/dailyRoundUtils"; import { downloadImage } from "@/utils/downloadImageWithDigestRouter"; -import { makeDataDumpToJson } from "@/utils/makeDataDump"; import { parseVitalsFromImage } from "@/utils/ocr"; +import { caclculateVitalsAccuracy } from "@/utils/vitalsAccuracy"; + const UPDATE_INTERVAL = 60 * 60 * 1000; @@ -293,7 +288,7 @@ export async function automatedDailyRounds() { } const _id = randomUUID(); - let vitals: DailyRoundObservation | null = s3SaveDailyRound + let vitals: DailyRoundObservation | null = saveVitalsStat ? null : await getVitalsFromObservations(monitor.ipAddress); @@ -349,7 +344,7 @@ export async function automatedDailyRounds() { console.log(`Vitals from image: ${JSON.stringify(vitals)}`); } - if (s3SaveDailyRound) { + if (saveVitalsStat) { const vitalsFromObservation = await getVitalsFromObservations( monitor.ipAddress, ); @@ -357,13 +352,36 @@ export async function automatedDailyRounds() { `Vitals from observations: ${JSON.stringify(vitalsFromObservation)}`, ); - makeDataDumpToJson( - { - vitalsFromObservation, - vitalsFromImage: vitals, - }, - `${hostname}/daily-rounds/${_id}.json`, - ); + const accuracy = caclculateVitalsAccuracy(vitals, vitalsFromObservation); + + if (accuracy !== null) { + console.log(`Accuracy: ${accuracy}%`); + + const lastVitalRecord = await prisma.vitalsStat.findFirst({ + orderBy: { createdAt: "desc" }, + }); + const weight = lastVitalRecord?.id; // number of records + const cumulativeAccuracy = lastVitalRecord + ? (weight! * lastVitalRecord.cumulativeAccuracy + accuracy) / + (weight! + 1) + : accuracy; + + prisma.vitalsStat.create({ + data: { + imageId: _id, + vitalsFromImage: JSON.parse(JSON.stringify(vitals)), + vitalsFromObservation: JSON.parse( + JSON.stringify(vitalsFromObservation), + ), + gptDetails: { + model: openaiVisionModel, + version: openaiApiVersion, + }, + accuracy, + cumulativeAccuracy, + }, + }); + } vitals = vitalsFromObservation ?? vitals; } diff --git a/src/cron/vitalsStatS3Dump.ts b/src/cron/vitalsStatS3Dump.ts new file mode 100644 index 0000000..342e3ac --- /dev/null +++ b/src/cron/vitalsStatS3Dump.ts @@ -0,0 +1,56 @@ +import fs from "fs"; +import path from "path"; + +import prisma from "@/lib/prisma"; +import { deleteVitalsStatOnDump, hostname } from "@/utils/configs"; +import { makeDataDumpToJson } from "@/utils/makeDataDump"; + +export async function vitalsStatS3Dump() { + // TODO: make the date range configurable + const toDate = new Date(); + const fromDate = new Date(toDate.getTime() - 24 * 60 * 60 * 1000); + + const vitalsStats = await prisma.vitalsStat.findMany({ + where: { + createdAt: { + gte: fromDate, + lte: toDate, + }, + }, + }); + + const dumpData = vitalsStats.map((vitalsStat) => { + const imageUrl = path.resolve("images", vitalsStat.imageId); + const image = fs.existsSync(imageUrl) + ? fs.readFileSync(imageUrl).toString("base64") + : null; + + return { + ...vitalsStat, + image, + }; + }); + + makeDataDumpToJson( + dumpData, + `${hostname}/vitals-stats/${fromDate.toISOString()}-${toDate.toISOString()}.json`, + ); + + if (deleteVitalsStatOnDump) { + await prisma.vitalsStat.deleteMany({ + where: { + createdAt: { + gte: fromDate, + lte: toDate, + }, + }, + }); + + vitalsStats.forEach((vitalsStat) => { + const imageUrl = path.resolve("images", vitalsStat.imageId); + if (fs.existsSync(imageUrl)) { + fs.unlinkSync(imageUrl); + } + }); + } +} diff --git a/src/index.ts b/src/index.ts index eb42161..4c189d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,10 @@ +import { vitalsStatS3Dump } from "./cron/vitalsStatS3Dump"; import * as cron from "node-cron"; import { automatedDailyRounds } from "@/cron/automatedDailyRounds"; import { retrieveAssetConfig } from "@/cron/retrieveAssetConfig"; import { initServer } from "@/server"; -import { port } from "@/utils/configs"; +import { port, s3DumpVitalsStat } from "@/utils/configs"; process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE = "1"; process.env.CHECKPOINT_DISABLE = "1"; @@ -16,9 +17,13 @@ process.env.CHECKPOINT_DISABLE = "1"; setTimeout(() => { retrieveAssetConfig(); - cron.schedule("0 */6 * * *", retrieveAssetConfig); + cron.schedule("0 */6 * * *", retrieveAssetConfig); // every 6 hours - cron.schedule("0 */1 * * *", automatedDailyRounds); + cron.schedule("0 */1 * * *", automatedDailyRounds); // every hour + + if (s3DumpVitalsStat) { + cron.schedule("0 0 * * *", vitalsStatS3Dump); // every day at midnight + } }, 100); server.listen(port, () => diff --git a/src/router/vitalsStatRouter.ts b/src/router/vitalsStatRouter.ts new file mode 100644 index 0000000..f088b5a --- /dev/null +++ b/src/router/vitalsStatRouter.ts @@ -0,0 +1,9 @@ +import express from "express"; + +import { VitalsStatController } from "@/controller/VitalsStatController"; + +const router = express.Router(); + +router.get("/accuracy", VitalsStatController.latestAccuracy); + +export { router as vitalsStatRouter }; diff --git a/src/server.ts b/src/server.ts index 4b962b5..45027e2 100644 --- a/src/server.ts +++ b/src/server.ts @@ -9,6 +9,8 @@ import helmet from "helmet"; import path from "path"; import swaggerUi from "swagger-ui-express"; + + import { OpenidConfigController } from "@/controller/OpenidConfigController"; import { ServerStatusController } from "@/controller/ServerStatusController"; import { randomString } from "@/lib/crypto"; @@ -27,6 +29,7 @@ import { healthRouter } from "@/router/healthRouter"; import { observationRouter } from "@/router/observationRouter"; import { serverStatusRouter } from "@/router/serverStatusRouter"; import { streamAuthApiRouter } from "@/router/streamAuthApiRouter"; +import { vitalsStatRouter } from "@/router/vitalsStatRouter"; import { swaggerSpec } from "@/swagger/swagger"; import type { WebSocket } from "@/types/ws"; import { @@ -110,6 +113,7 @@ export function initServer() { app.use("/assets", assetConfigRouter); app.use("/api/assets", assetConfigApiRouter); app.use("/api/stream", streamAuthApiRouter); + app.use("/api/vitals-stats", vitalsStatRouter); app.get("/.well-known/jwks.json", OpenidConfigController.publicJWKs); app.get( @@ -135,4 +139,4 @@ export function initServer() { ServerStatusController.init(ws); return app; -} +} \ No newline at end of file diff --git a/src/utils/configs.ts b/src/utils/configs.ts index 99a9e36..a5a4772 100644 --- a/src/utils/configs.ts +++ b/src/utils/configs.ts @@ -21,6 +21,7 @@ export const sentryTracesSampleRate = parseFloat( ); export const saveDailyRound = Boolean(process.env.SAVE_DAILY_ROUND ?? "true"); +export const saveVitalsStat = Boolean(process.env.SAVE_VITALS_STAT ?? "true"); export const s3Provider = process.env.S3_PROVIDER ?? "AWS"; export const s3Endpoint = @@ -33,11 +34,12 @@ export const s3BucketName = process.env.S3_BUCKET_NAME; export const s3AccessKeyId = process.env.S3_ACCESS_KEY_ID; export const s3SecretAccessKey = process.env.S3_SECRET_ACCESS_KEY; -export const s3SaveDailyRound = - Boolean(process.env.S3_SAVE_DAILY_ROUND) && - s3BucketName && - s3AccessKeyId && - s3SecretAccessKey; +export const s3DumpVitalsStat = Boolean( + process.env.S3_DUMP_VITALS_STAT ?? "false", +); +export const deleteVitalsStatOnDump = Boolean( + process.env.DELETE_VITALS_STAT_ON_DUMP ?? "false", +); export const openaiApiKey = process.env.OPENAI_API_KEY ?? ""; export const openaiEndpoint = process.env.OPENAI_ENDPOINT ?? ""; diff --git a/src/utils/vitalsAccuracy.ts b/src/utils/vitalsAccuracy.ts new file mode 100644 index 0000000..b662bb4 --- /dev/null +++ b/src/utils/vitalsAccuracy.ts @@ -0,0 +1,71 @@ +import { DailyRoundObservation } from "@/types/observation"; + +type ComparisonType = "relative" | "fixed"; + +function calculateAccuracy( + obj1: Object, + obj2: Object, + keysToCompare: string[], + comparisonType: ComparisonType = "relative", +) { + function compareValues( + value1: number, + value2: number, + comparisonType: ComparisonType = "relative", + ) { + if ( + value1 === null || + value2 === null || + value1 === undefined || + value2 === undefined || + isNaN(value1) || + isNaN(value2) + ) { + return 0; + } + + if (comparisonType === "relative") { + const maxDiff = Math.max(Math.abs(value1), Math.abs(value2)); + const diff = Math.abs(value1 - value2); + return maxDiff === 0 ? 1 : 1 - diff / maxDiff; + } else { + return value1 === value2 ? 1 : 0; + } + } + + function getValue(obj: any, key: string): any { + return key.split(".").reduce((o, k) => (o ? o[k] : undefined), obj); + } + + let totalScore = 0; + + for (const key of keysToCompare) { + const value1 = getValue(obj1, key); + const value2 = getValue(obj2, key); + totalScore += compareValues(value1, value2, comparisonType); + } + + return (totalScore / keysToCompare.length) * 100; +} + +export function caclculateVitalsAccuracy( + vitals: DailyRoundObservation | null | undefined, + original: DailyRoundObservation | null | undefined, + type: ComparisonType = "relative", +) { + if (!vitals || !original) { + return null; + } + + const keysToCompare = [ + "spo2", + "ventilator_spo2", + "resp", + "pulse", + "temperature", + "bp.systolic", + "bp.diastolic", + ]; + + return calculateAccuracy(vitals, original, keysToCompare, type); +} From ffbe2b55af2b488a8bf4db94f59b0b888f92a154 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sun, 9 Jun 2024 19:18:32 +0530 Subject: [PATCH 4/7] convert observations s3 dump into a cron --- src/controller/ObservationController.ts | 39 +++++++------------------ src/cron/observationsS3Dump.ts | 18 ++++++++++++ src/index.ts | 4 +++ src/utils/makeDataDump.ts | 15 ++++------ 4 files changed, 38 insertions(+), 38 deletions(-) create mode 100644 src/cron/observationsS3Dump.ts diff --git a/src/controller/ObservationController.ts b/src/controller/ObservationController.ts index 15fc69f..010740b 100644 --- a/src/controller/ObservationController.ts +++ b/src/controller/ObservationController.ts @@ -1,18 +1,20 @@ import type { Request, Response } from "express"; - - import { BadRequestException } from "@/Exception/BadRequestException"; import { NotFoundException } from "@/Exception/NotFoundException"; -import type { DailyRoundObservation, LastObservationData, Observation, ObservationStatus, ObservationType, ObservationTypeWithWaveformTypes, StaticObservation } from "@/types/observation"; +import type { + LastObservationData, + Observation, + ObservationStatus, + ObservationType, + ObservationTypeWithWaveformTypes, + StaticObservation, +} from "@/types/observation"; import { WebSocket } from "@/types/ws"; import { ObservationsMap } from "@/utils/ObservationsMap"; import { catchAsync } from "@/utils/catchAsync"; -import { hostname } from "@/utils/configs"; -import { makeDataDumpToJson } from "@/utils/makeDataDump"; import { filterClients } from "@/utils/wsUtils"; - export var staticObservations: StaticObservation[] = []; var activeDevices: string[] = []; var lastRequestData = {}; @@ -22,31 +24,10 @@ var logData: { }[] = []; var statusData: ObservationStatus[] = []; var lastObservationData: LastObservationData = {}; -let observationData: { time: Date; data: Observation[][] }[] = []; +export let observationData: { time: Date; data: Observation[][] }[] = []; - -const S3_DATA_DUMP_INTERVAL = 1000 * 60 * 60; const DEFAULT_LISTING_LIMIT = 10; -setInterval(() => { - makeDataDumpToJson( - observationData, - `${hostname}/${new Date().getTime()}.json`, - { - slug: "s3_observations_dump", - options: { - schedule: { - type: "interval", - unit: "minutes", - value: S3_DATA_DUMP_INTERVAL / (1000 * 60), - }, - }, - }, - ); - - observationData = []; -}, S3_DATA_DUMP_INTERVAL); - const getTime = (date: string) => new Date(date.replace(" ", "T").concat("+0530")); @@ -296,4 +277,4 @@ export class ObservationController { filterStatusData(); return res.json(statusData); }); -} \ No newline at end of file +} diff --git a/src/cron/observationsS3Dump.ts b/src/cron/observationsS3Dump.ts new file mode 100644 index 0000000..3f4b254 --- /dev/null +++ b/src/cron/observationsS3Dump.ts @@ -0,0 +1,18 @@ +import { observationData } from "@/controller/ObservationController"; +import { hostname } from "@/utils/configs"; +import { makeDataDumpToJson } from "@/utils/makeDataDump"; + +export async function observationsS3Dump() { + const data = [...observationData]; + makeDataDumpToJson(data, `${hostname}/${new Date().getTime()}.json`, { + slug: "s3_observations_dump", + options: { + schedule: { + type: "crontab", + value: "30 * * * *", + }, + }, + }); + + observationData.splice(0, data.length); +} diff --git a/src/index.ts b/src/index.ts index 4c189d7..6faa718 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +import { observationsS3Dump } from "./cron/observationsS3Dump"; import { vitalsStatS3Dump } from "./cron/vitalsStatS3Dump"; import * as cron from "node-cron"; @@ -21,6 +22,9 @@ process.env.CHECKPOINT_DISABLE = "1"; cron.schedule("0 */1 * * *", automatedDailyRounds); // every hour + // scheduled to run at 30th minute of every hour so that the automatedDailyRounds can use the data without any issues + cron.schedule("30 * * * *", observationsS3Dump); // every hour (30th minute) + if (s3DumpVitalsStat) { cron.schedule("0 0 * * *", vitalsStatS3Dump); // every day at midnight } diff --git a/src/utils/makeDataDump.ts b/src/utils/makeDataDump.ts index 0c52d38..426b6ea 100644 --- a/src/utils/makeDataDump.ts +++ b/src/utils/makeDataDump.ts @@ -1,20 +1,17 @@ import { captureCheckIn } from "@sentry/node"; import AWS from "aws-sdk"; -import { - s3AccessKeyId, - s3BucketName, - s3Endpoint, - s3Provider, - s3SecretAccessKey, -} from "@/utils/configs"; + + +import { s3AccessKeyId, s3BucketName, s3Endpoint, s3Provider, s3SecretAccessKey } from "@/utils/configs"; + export const makeDataDumpToJson = async ( data: Record | any[], key: string, monitorOptions?: { slug: string; - options?: any; + options?: Parameters[1]; }, ) => { let checkInId: string | undefined = undefined; @@ -80,4 +77,4 @@ export const makeDataDumpToJson = async ( } console.log(err); } -}; +}; \ No newline at end of file From dc27213d36b0f4c73a65e1c1e2ae32b7f4669336 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sun, 9 Jun 2024 21:31:24 +0530 Subject: [PATCH 5/7] parse time from image --- src/cron/automatedDailyRounds.ts | 28 +++++++++++++++++++--------- src/utils/ocr.ts | 13 ++++--------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/cron/automatedDailyRounds.ts b/src/cron/automatedDailyRounds.ts index 08fefc0..b9a70f8 100644 --- a/src/cron/automatedDailyRounds.ts +++ b/src/cron/automatedDailyRounds.ts @@ -5,7 +5,10 @@ import path from "path"; -import { staticObservations } from "@/controller/ObservationController"; +import { + observationData, + staticObservations, +} from "@/controller/ObservationController"; import prisma from "@/lib/prisma"; import { AssetBed } from "@/types/asset"; import { CameraParams } from "@/types/camera"; @@ -26,6 +29,7 @@ import { parseVitalsFromImage } from "@/utils/ocr"; import { caclculateVitalsAccuracy } from "@/utils/vitalsAccuracy"; + const UPDATE_INTERVAL = 60 * 60 * 1000; export async function getMonitorPreset(bedId: string, assetId: string) { @@ -92,12 +96,11 @@ export async function getVitalsFromImage(imageUrl: string) { return null; } - // const date = data.time_stamp ? new Date(data.time_stamp) : new Date(); - // const isoDate = - // date.toString() !== "Invalid Date" - // ? date.toISOString() - // : new Date().toISOString(); - const isoDate = new Date().toISOString(); + const date = data.time_stamp ? new Date(data.time_stamp) : new Date(); + const isoDate = + date.toString() !== "Invalid Date" + ? date.toISOString() + : new Date().toISOString(); const payload = { taken_at: isoDate, @@ -293,12 +296,14 @@ export async function automatedDailyRounds() { : await getVitalsFromObservations(monitor.ipAddress); console.log( - saveDailyRound - ? "Skipping vitals from observations as saving daily round is enabled" + saveVitalsStat + ? "Skipping vitals from observations as saving vitals stat is enabled" : `Vitals from observations: ${JSON.stringify(vitals)}`, ); if (!vitals && openaiApiKey) { + console.log(`Getting vitals from camera for the patient ${patient_id}`); + if (!asset_beds || asset_beds.length === 0) { console.error( `No asset beds found for the asset ${monitor.externalId}`, @@ -345,6 +350,7 @@ export async function automatedDailyRounds() { } if (saveVitalsStat) { + // TODO: get the nearest observation and parse it as vitals const vitalsFromObservation = await getVitalsFromObservations( monitor.ipAddress, ); @@ -352,6 +358,7 @@ export async function automatedDailyRounds() { `Vitals from observations: ${JSON.stringify(vitalsFromObservation)}`, ); + // TODO: update the vitals accuracy calculation const accuracy = caclculateVitalsAccuracy(vitals, vitalsFromObservation); if (accuracy !== null) { @@ -366,6 +373,7 @@ export async function automatedDailyRounds() { (weight! + 1) : accuracy; + // TODO: update the db schema prisma.vitalsStat.create({ data: { imageId: _id, @@ -383,9 +391,11 @@ export async function automatedDailyRounds() { }); } + // TODO: get the vitals from the observation and update the vitals vitals = vitalsFromObservation ?? vitals; } + // TODO: move this check into the respective vitals functions if (!vitals || !payloadHasData(vitals)) { console.error(`No vitals found for the patient ${patient_id}`); return; diff --git a/src/utils/ocr.ts b/src/utils/ocr.ts index 97c7ebd..041385e 100644 --- a/src/utils/ocr.ts +++ b/src/utils/ocr.ts @@ -1,13 +1,8 @@ -import { - openaiApiKey, - openaiApiVersion, - openaiEndpoint, - openaiUseAzure, - openaiVisionModel, -} from "./configs"; +import { openaiApiKey, openaiApiVersion, openaiEndpoint, openaiUseAzure, openaiVisionModel } from "./configs"; import { AzureOpenAI, OpenAI } from "openai"; import sharp from "sharp"; + const openai = openaiUseAzure ? new AzureOpenAI({ apiKey: openaiApiKey, @@ -48,7 +43,7 @@ export async function parseVitalsFromImage(image: Buffer) { NOTE: Many fields from below example can be missing, you need to output null for those fields. Example output in minified JSON format: - {"time_stamp":"yyyy-mm-ddThh:mm:ss","ecg":{"Heart_Rate_bpm":},"nibp":{"systolic_mmhg":,"diastolic_mmhg":,"mean_arterial_pressure_mmhg":},"spO2":{"oxygen_saturation_percentage":},"respiration_rate":{"breaths_per_minute":},"temperature":{"fahrenheit":}} + {"time_stamp":"yyyy-mm-ddThh:mm:ssZ","ecg":{"Heart_Rate_bpm":},"nibp":{"systolic_mmhg":,"diastolic_mmhg":,"mean_arterial_pressure_mmhg":},"spO2":{"oxygen_saturation_percentage":},"respiration_rate":{"breaths_per_minute":},"temperature":{"fahrenheit":}} The output should be minified JSON format only. `.trim(), @@ -80,4 +75,4 @@ export async function parseVitalsFromImage(image: Buffer) { console.error("Failed to get response from OpenAI", error); return null; } -} +} \ No newline at end of file From 627ca665a57788f5db701374b52f3ee42724e97b Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sun, 9 Jun 2024 22:46:47 +0530 Subject: [PATCH 6/7] calculate accuracy for each field and track fp and fn --- .../migration.sql | 16 ++++++ prisma/schema.prisma | 4 +- src/cron/automatedDailyRounds.ts | 39 ++++++++++---- src/utils/vitalsAccuracy.ts | 53 ++++++++++++++++--- 4 files changed, 93 insertions(+), 19 deletions(-) create mode 100644 prisma/migrations/20240609170140_change_accuracy_to_json/migration.sql diff --git a/prisma/migrations/20240609170140_change_accuracy_to_json/migration.sql b/prisma/migrations/20240609170140_change_accuracy_to_json/migration.sql new file mode 100644 index 0000000..f977e26 --- /dev/null +++ b/prisma/migrations/20240609170140_change_accuracy_to_json/migration.sql @@ -0,0 +1,16 @@ +-- Rename the existing column to a temporary name +ALTER TABLE "VitalsStat" RENAME COLUMN "accuracy" TO "accuracy_temp"; +ALTER TABLE "VitalsStat" RENAME COLUMN "cumulativeAccuracy" TO "cumulativeAccuracy_temp"; + +-- Add the new column with the Json type +ALTER TABLE "VitalsStat" ADD COLUMN "accuracy" Json; +ALTER TABLE "VitalsStat" ADD COLUMN "cumulativeAccuracy" Json; + +-- Copy the data from the old column to the new column, converting floats to JSON +UPDATE "VitalsStat" SET "accuracy" = json_build_object('overall', "accuracy_temp", 'metrics', '[]'::json); +UPDATE "VitalsStat" SET "cumulativeAccuracy" = json_build_object('overall', "cumulativeAccuracy_temp", 'metrics', '[]'::json); + +-- Drop the temporary column +ALTER TABLE "VitalsStat" DROP COLUMN "accuracy_temp"; +ALTER TABLE "VitalsStat" DROP COLUMN "cumulativeAccuracy_temp"; + diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bf59840..6b00ef2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -72,8 +72,8 @@ model VitalsStat { vitalsFromObservation Json vitalsFromImage Json gptDetails Json - accuracy Float - cumulativeAccuracy Float + accuracy Json + cumulativeAccuracy Json createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } diff --git a/src/cron/automatedDailyRounds.ts b/src/cron/automatedDailyRounds.ts index b9a70f8..74cfbf7 100644 --- a/src/cron/automatedDailyRounds.ts +++ b/src/cron/automatedDailyRounds.ts @@ -26,7 +26,7 @@ import { careApi, openaiApiKey, openaiApiVersion, openaiVisionModel, saveDailyRo import { getPatientId } from "@/utils/dailyRoundUtils"; import { downloadImage } from "@/utils/downloadImageWithDigestRouter"; import { parseVitalsFromImage } from "@/utils/ocr"; -import { caclculateVitalsAccuracy } from "@/utils/vitalsAccuracy"; +import { Accuracy, calculateVitalsAccuracy } from "@/utils/vitalsAccuracy"; @@ -358,22 +358,43 @@ export async function automatedDailyRounds() { `Vitals from observations: ${JSON.stringify(vitalsFromObservation)}`, ); - // TODO: update the vitals accuracy calculation - const accuracy = caclculateVitalsAccuracy(vitals, vitalsFromObservation); + const accuracy = calculateVitalsAccuracy(vitals, vitalsFromObservation); if (accuracy !== null) { - console.log(`Accuracy: ${accuracy}%`); + console.log(`Accuracy: ${accuracy.overall}%`); const lastVitalRecord = await prisma.vitalsStat.findFirst({ orderBy: { createdAt: "desc" }, }); const weight = lastVitalRecord?.id; // number of records - const cumulativeAccuracy = lastVitalRecord - ? (weight! * lastVitalRecord.cumulativeAccuracy + accuracy) / - (weight! + 1) - : accuracy; + const cumulativeAccuracy = ( + lastVitalRecord?.cumulativeAccuracy as Accuracy + ).metrics.map((metric) => { + const latestMetric = accuracy.metrics.find( + (m) => m.field === metric.field, + ); + + return { + ...metric, + accuracy: lastVitalRecord + ? (metric.accuracy * weight! + latestMetric?.accuracy!) / + (weight! + 1) + : latestMetric?.accuracy!, + falsePositive: + lastVitalRecord && latestMetric?.falsePositive + ? (metric.falsePositive! * weight! + + latestMetric?.falsePositive!) / + (weight! + 1) + : metric.falsePositive, + falseNegative: + lastVitalRecord && latestMetric?.falseNegative + ? (metric.falseNegative! * weight! + + latestMetric?.falseNegative!) / + (weight! + 1) + : metric.falseNegative, + }; + }); - // TODO: update the db schema prisma.vitalsStat.create({ data: { imageId: _id, diff --git a/src/utils/vitalsAccuracy.ts b/src/utils/vitalsAccuracy.ts index b662bb4..7fe6369 100644 --- a/src/utils/vitalsAccuracy.ts +++ b/src/utils/vitalsAccuracy.ts @@ -1,13 +1,23 @@ import { DailyRoundObservation } from "@/types/observation"; + type ComparisonType = "relative" | "fixed"; +type AccuracyMetrics = { + field: string; + accuracy: number; + falsePositive: number; + falseNegative: number; +}; + +export type Accuracy = { overall: number; metrics: AccuracyMetrics[] }; + function calculateAccuracy( obj1: Object, obj2: Object, keysToCompare: string[], comparisonType: ComparisonType = "relative", -) { +): AccuracyMetrics[] { function compareValues( value1: number, value2: number, @@ -37,22 +47,41 @@ function calculateAccuracy( return key.split(".").reduce((o, k) => (o ? o[k] : undefined), obj); } - let totalScore = 0; + const metrics: AccuracyMetrics[] = []; for (const key of keysToCompare) { const value1 = getValue(obj1, key); const value2 = getValue(obj2, key); - totalScore += compareValues(value1, value2, comparisonType); + const accuracy = compareValues(value1, value2, comparisonType); + const falsePositive = + (value1 === null || value1 === undefined) && + value2 !== null && + value2 !== undefined + ? 1 + : 0; + const falseNegative = + value1 !== null && + value1 !== undefined && + (value2 === null || value2 === undefined) + ? 1 + : 0; + + metrics.push({ + field: key, + accuracy, + falsePositive, + falseNegative, + }); } - return (totalScore / keysToCompare.length) * 100; + return metrics; } -export function caclculateVitalsAccuracy( +export function calculateVitalsAccuracy( vitals: DailyRoundObservation | null | undefined, original: DailyRoundObservation | null | undefined, type: ComparisonType = "relative", -) { +): Accuracy | null { if (!vitals || !original) { return null; } @@ -67,5 +96,13 @@ export function caclculateVitalsAccuracy( "bp.diastolic", ]; - return calculateAccuracy(vitals, original, keysToCompare, type); -} + const metrics = calculateAccuracy(vitals, original, keysToCompare, type); + const overall = + metrics.reduce((acc, curr) => acc + curr.accuracy, 0) / + keysToCompare.length; + + return { + overall: overall * 100, + metrics, + }; +} \ No newline at end of file From 1ebcca25450b7605925cec2573b193174eb1fcf2 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 10 Jun 2024 00:03:23 +0530 Subject: [PATCH 7/7] get the latest observation data for accuracy calculation --- src/cron/automatedDailyRounds.ts | 106 +++++++++++++++++++++++++------ 1 file changed, 85 insertions(+), 21 deletions(-) diff --git a/src/cron/automatedDailyRounds.ts b/src/cron/automatedDailyRounds.ts index 74cfbf7..fb2e42c 100644 --- a/src/cron/automatedDailyRounds.ts +++ b/src/cron/automatedDailyRounds.ts @@ -5,19 +5,12 @@ import path from "path"; -import { - observationData, - staticObservations, -} from "@/controller/ObservationController"; +import { observationData, staticObservations } from "@/controller/ObservationController"; import prisma from "@/lib/prisma"; import { AssetBed } from "@/types/asset"; import { CameraParams } from "@/types/camera"; import { CarePaginatedResponse } from "@/types/care"; -import { - DailyRoundObservation, - Observation, - ObservationType, -} from "@/types/observation"; +import { DailyRoundObservation, Observation, ObservationType } from "@/types/observation"; import { OCRV2Response } from "@/types/ocr"; import { CameraUtils } from "@/utils/CameraUtils"; import { isValid } from "@/utils/ObservationUtils"; @@ -29,7 +22,6 @@ import { parseVitalsFromImage } from "@/utils/ocr"; import { Accuracy, calculateVitalsAccuracy } from "@/utils/vitalsAccuracy"; - const UPDATE_INTERVAL = 60 * 60 * 1000; export async function getMonitorPreset(bedId: string, assetId: string) { @@ -130,7 +122,7 @@ export async function getVitalsFromImage(imageUrl: string) { payload.bp = {}; } - return payload; + return payloadHasData(payload) ? payload : null; } export async function fileAutomatedDailyRound( @@ -234,7 +226,7 @@ export async function getVitalsFromObservations(assetHostname: string) { } const data = observation.observations; - return { + const vitals = { taken_at: observation.last_updated, spo2: getValueFromData("SpO2", data), ventilator_spo2: getValueFromData("SpO2", data), @@ -251,6 +243,8 @@ export async function getVitalsFromObservations(assetHostname: string) { rounds_type: "AUTOMATED", is_parsed_by_ocr: false, } as DailyRoundObservation; + + return payloadHasData(vitals) ? vitals : null; } export function payloadHasData(payload: Record): boolean { @@ -267,6 +261,72 @@ export function payloadHasData(payload: Record): boolean { }); } +export function getVitalsFromObservationsForAccuracy( + deviceId: string, + time: string, +) { + // TODO: consider optimizing this + const observations = observationData + .reduce((acc, curr) => { + return [...acc, ...curr.data]; + }, [] as Observation[][]) + .find( + (observation) => + observation[0].device_id === deviceId && + new Date(observation[0]["date-time"]).toISOString() === + new Date(time).toISOString(), + ); + + if (!observations) { + return null; + } + + const vitals = observations.reduce( + (acc, curr) => { + switch (curr.observation_id) { + case "SpO2": + return { ...acc, spo2: curr.value, ventilator_spo2: curr.value }; + case "respiratory-rate": + return { ...acc, resp: curr.value }; + case "heart-rate": + return { ...acc, pulse: curr.value ?? acc.pulse }; + case "pulse-rate": + return { ...acc, pulse: acc.pulse ?? curr.value }; + case "body-temperature1": + return { + ...acc, + temperature: curr.value ?? acc.temperature, + temperature_measured_at: curr["date-time"], + }; + case "body-temperature2": + return { + ...acc, + temperature: acc.temperature ?? curr.value, + temperature_measured_at: curr["date-time"], + }; + case "blood-pressure": + return { + ...acc, + bp: { + systolic: curr.systolic.value, + diastolic: curr.diastolic.value, + map: curr.map?.value, + }, + }; + default: + return acc; + } + }, + { + taken_at: time, + rounds_type: "AUTOMATED", + is_parsed_by_ocr: false, + } as DailyRoundObservation, + ); + + return payloadHasData(vitals) ? vitals : null; +} + export async function automatedDailyRounds() { console.log("Automated daily rounds"); const monitors = await prisma.asset.findMany({ @@ -349,13 +409,13 @@ export async function automatedDailyRounds() { console.log(`Vitals from image: ${JSON.stringify(vitals)}`); } - if (saveVitalsStat) { - // TODO: get the nearest observation and parse it as vitals - const vitalsFromObservation = await getVitalsFromObservations( + if (vitals && saveVitalsStat) { + const vitalsFromObservation = await getVitalsFromObservationsForAccuracy( monitor.ipAddress, + new Date(vitals.taken_at!).toISOString(), ); console.log( - `Vitals from observations: ${JSON.stringify(vitalsFromObservation)}`, + `Vitals from observations for accuracy: ${JSON.stringify(vitalsFromObservation)}`, ); const accuracy = calculateVitalsAccuracy(vitals, vitalsFromObservation); @@ -411,13 +471,17 @@ export async function automatedDailyRounds() { }, }); } - - // TODO: get the vitals from the observation and update the vitals - vitals = vitalsFromObservation ?? vitals; } - // TODO: move this check into the respective vitals functions - if (!vitals || !payloadHasData(vitals)) { + const vitalsFromObservation = await getVitalsFromObservations( + monitor.ipAddress, + ); + console.log( + `Vitals from observations: ${JSON.stringify(vitalsFromObservation)}`, + ); + vitals = vitalsFromObservation ?? vitals; + + if (!vitals) { console.error(`No vitals found for the patient ${patient_id}`); return; }