Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
1f2f64f
Start working on vitallens-2.0 support
prouast Sep 10, 2025
5b60c21
Update tests and note TODOs
prouast Sep 10, 2025
2f5b546
Work on VitalsEstimateManager tests
prouast Sep 10, 2025
7de480c
Fix assembleResult tests in VitalsEstimateManager
prouast Sep 11, 2025
4eb6df6
Implement simple peak detector
prouast Sep 11, 2025
8605e8c
Re-implement HRV
prouast Sep 12, 2025
6afc751
Update VitalsEstimateManager test
prouast Sep 12, 2025
b9bb278
Fix bug
prouast Sep 13, 2025
1811eaf
Remove nodemon
prouast Sep 13, 2025
56382e6
Fix issue with ffmpeg init
prouast Sep 13, 2025
35cd856
Fix issue with minimum frames initially
prouast Sep 13, 2025
37d25eb
Add test for nms
prouast Sep 13, 2025
876ac14
Add test for BufferedResultsConsumer
prouast Sep 13, 2025
e0a3a9b
Update README
prouast Sep 13, 2025
3024ebe
Add X-Model to CORS headers in local test server
prouast Sep 13, 2025
02aab75
Start working on new minimal widget; Add HRV to existing widgets; Fix…
prouast Sep 16, 2025
2082e1d
Fix bugs; Improve peak det and HRV calc; Remove LF/HF from web component
prouast Sep 17, 2025
d738c4b
Handle network instability with fallback to /file and graceful reset
prouast Sep 22, 2025
5ec24de
Add missing args
prouast Sep 22, 2025
c79de5b
Debug web components
prouast Sep 22, 2025
404a782
Disable debug log
prouast Sep 22, 2025
ddcde4c
Remove eco-mode setting
prouast Sep 22, 2025
5ff7dd4
Fix examples
prouast Oct 10, 2025
a8385af
Use peak confidence thresholding in HRV calculation
prouast Oct 10, 2025
b544bcd
Get vitals monitor ready
prouast Oct 10, 2025
97ffe30
Move HRV display in widget
prouast Oct 10, 2025
94fa874
Show low confidence face status
prouast Oct 10, 2025
85eab2f
Add support for VitalLens 1.1 model
prouast Oct 31, 2025
49cfb5b
Make linter happy
prouast Oct 31, 2025
f2c47e5
Update README for new widget structure
prouast Oct 31, 2025
1f9919e
Bump jest and @types/jest
dependabot[bot] Nov 1, 2025
d1b4c5c
Remove usage of deprecated jest features
prouast Nov 1, 2025
894c880
Bump jest-environment-jsdom from 29.7.0 to 30.2.0
dependabot[bot] Nov 1, 2025
3cdb4b9
Bump the development-dependencies group with 14 updates
dependabot[bot] Nov 1, 2025
9c5f21b
Bump actions/setup-node from 5 to 6
dependabot[bot] Nov 1, 2025
e179420
Bump chart.js
prouast Nov 2, 2025
02eb6b1
Improved usage samples in README
prouast Nov 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Tests

on:
push:
branches: ['*']
branches: [dev, main]
pull_request:
branches: ['main']
branches: [main]

jobs:
linux:
Expand All @@ -19,7 +19,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}

Expand Down Expand Up @@ -60,7 +60,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: "18.16.1"

Expand Down
199 changes: 90 additions & 109 deletions README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions examples/browser/file_widget.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<title>VitalLens Widget Example</title>
<style>body { background: black; }</style>
<script type="module" src="../vitallens.browser.js"></script>
<link rel="preconnect" href="https://api.rouast.com" crossorigin>
</head>
<body>
<vitallens-file-widget api-key="YOUR_API_KEY"></vitallens-file-widget>
Expand Down
13 changes: 13 additions & 0 deletions examples/browser/vitals_monitor.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>VitalLens Vitals Monitor</title>
<style>body { background: black; }</style>
<script type="module" src="../vitallens.browser.js"></script>
<link rel="preconnect" href="https://api.rouast.com" crossorigin>
</head>
<body>
<vitallens-vitals-monitor api-key="YOUR_API_KEY"></vitallens-vitals-monitor>
</body>
</html>
11,948 changes: 7,844 additions & 4,104 deletions package-lock.json

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vitallens",
"version": "0.1.3",
"version": "0.2.3",
"description": "VitalLens: Estimate vital signs such as heart rate and respiratory rate from face video.",
"main": "./dist/vitallens.cjs.js",
"module": "./dist/vitallens.esm.js",
Expand Down Expand Up @@ -31,11 +31,12 @@
"test:node-integration": "npm run build:integration && jest --selectProjects node-integration",
"lint": "eslint src/**/*.ts",
"format": "prettier --write \"src/**/*.{ts,js,json,css,md}\"",
"start:browser": "EXAMPLE_TO_OPEN=browser/widget nodemon scripts/server.cjs",
"start:browser-file": "EXAMPLE_TO_OPEN=browser/file_widget nodemon scripts/server.cjs",
"start:browser-file-minimal": "EXAMPLE_TO_OPEN=browser/file_minimal nodemon scripts/server.cjs",
"start:browser-webcam": "EXAMPLE_TO_OPEN=browser/webcam_widget nodemon scripts/server.cjs",
"start:browser-webcam-minimal": "EXAMPLE_TO_OPEN=browser/webcam_minimal nodemon scripts/server.cjs",
"start:browser": "EXAMPLE_TO_OPEN=browser/vitals_monitor node scripts/server.cjs",
"start:browser-widget": "EXAMPLE_TO_OPEN=browser/widget node scripts/server.cjs",
"start:browser-widget-file": "EXAMPLE_TO_OPEN=browser/file_widget node scripts/server.cjs",
"start:browser-file-minimal": "EXAMPLE_TO_OPEN=browser/file_minimal node scripts/server.cjs",
"start:browser-widget-webcam": "EXAMPLE_TO_OPEN=browser/webcam_widget node scripts/server.cjs",
"start:browser-webcam-minimal": "EXAMPLE_TO_OPEN=browser/webcam_minimal node scripts/server.cjs",
"start:node-file": "node ./examples/node/file.js"
},
"keywords": [
Expand Down Expand Up @@ -111,7 +112,7 @@
"@tensorflow/tfjs-core": "^4.22.0",
"@tensorflow/tfjs-node": "^4.22.0",
"@types/fluent-ffmpeg": "^2.1.27",
"@types/jest": "^29.5.14",
"@types/jest": "^30.0.0",
"@types/puppeteer": "^7.0.4",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.35.0",
Expand All @@ -124,11 +125,10 @@
"eslint-plugin-prettier": "^5.5.4",
"express": "^5.1.0",
"globals": "^16.2.0",
"jest": "^29.7.0",
"jest": "^30.2.0",
"jest-dev-server": "^11.0.0",
"jest-environment-jsdom": "^29.7.0",
"jest-environment-jsdom": "^30.2.0",
"jest-puppeteer": "^11.0.0",
"nodemon": "^3.1.9",
"prettier": "^3.5.3",
"puppeteer": "^24.17.0",
"rollup": "^4.44.0",
Expand Down
36 changes: 16 additions & 20 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ const faceDetectionWorkerBrowserConfig = {
preventAssignment: true,
values: {
SELF_CONTAINED_BUILD: JSON.stringify(false),
__FFMPEG_CORE_URL__: JSON.stringify(''),
__FFMPEG_WASM_URL__: JSON.stringify(''),
__FFMPEG_CORE_URL__: '',
__FFMPEG_WASM_URL__: '',
},
}),
alias({
Expand Down Expand Up @@ -208,8 +208,8 @@ const browserConfig = {
preventAssignment: true,
values: {
SELF_CONTAINED_BUILD: JSON.stringify(false),
__FFMPEG_CORE_URL__: JSON.stringify(''),
__FFMPEG_WASM_URL__: JSON.stringify(''),
__FFMPEG_CORE_URL__: '',
__FFMPEG_WASM_URL__: '',
},
}),
alias({
Expand Down Expand Up @@ -260,23 +260,19 @@ const browserSelfContainedConfig = {
preventAssignment: true,
values: {
SELF_CONTAINED_BUILD: JSON.stringify(true),
__FFMPEG_CORE_URL__: JSON.stringify(
toDataURI(
path.resolve(
__dirname,
'node_modules/@ffmpeg/core/dist/esm/ffmpeg-core.js'
),
'text/javascript'
)
__FFMPEG_CORE_URL__: toDataURI(
path.resolve(
__dirname,
'node_modules/@ffmpeg/core/dist/esm/ffmpeg-core.js'
),
'text/javascript'
),
__FFMPEG_WASM_URL__: JSON.stringify(
toDataURI(
path.resolve(
__dirname,
'node_modules/@ffmpeg/core/dist/esm/ffmpeg-core.wasm'
),
'application/wasm'
)
__FFMPEG_WASM_URL__: toDataURI(
path.resolve(
__dirname,
'node_modules/@ffmpeg/core/dist/esm/ffmpeg-core.wasm'
),
'application/wasm'
),
},
}),
Expand Down
2 changes: 1 addition & 1 deletion scripts/server.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ app.use((_, res, next) => {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers':
'Origin, X-Requested-With, X-Origin, X-State, X-Encoding, Content-Type, Accept, Range',
'Origin, X-Requested-With, X-Origin, X-State, X-Model, X-Encoding, Content-Type, Accept, Range',
'Content-Security-Policy':
"default-src 'self' blob: data:; " +
"script-src-elem 'self' 'unsafe-inline' 'unsafe-eval' blob: https://unpkg.com/ https://cdn.jsdelivr.net; " +
Expand Down
9 changes: 9 additions & 0 deletions src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export const VITALLENS_FILE_ENDPOINT =
'https://api.rouast.com/vitallens-v3/file';
export const VITALLENS_STREAM_ENDPOINT =
'https://api.rouast.com/vitallens-v3/stream';
export const VITALLENS_RESOLVE_MODEL_ENDPOINT =
'https://api.rouast.com/vitallens-v3/resolve-model';

// Vitals estimation constraints [1/min]
export const CALC_HR_MIN = 40;
Expand All @@ -17,6 +19,13 @@ export const CALC_HR_WINDOW_SIZE = 10;
export const CALC_HR_MIN_WINDOW_SIZE = 5;
export const CALC_RR_WINDOW_SIZE = 30;
export const CALC_RR_MIN_WINDOW_SIZE = 10;
export const CALC_HRV_SDNN_MIN_T = 20;
export const CALC_HRV_RMSSD_MIN_T = 20;
export const CALC_HRV_LFHF_MIN_T = 55;

// HRV Frequency Bands [Hz]
export const HRV_LF_BAND: [number, number] = [0.04, 0.15];
export const HRV_HF_BAND: [number, number] = [0.15, 0.4];

// Face detection defaults [Hz]
export const FDET_DEFAULT_FS_FILE = 0.5;
Expand Down
14 changes: 3 additions & 11 deletions src/config/methodsConfig.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,6 @@
import { MethodConfig } from '../types';

export const METHODS_CONFIG: Record<string, MethodConfig> = {
vitallens: {
method: 'vitallens',
roiMethod: 'upper_body',
fpsTarget: 30,
inputSize: 40,
minWindowLength: 16,
minWindowLengthState: 4,
maxWindowLength: 900,
requiresState: true,
bufferOffset: 1.5,
},
pos: {
method: 'pos',
roiMethod: 'face',
Expand All @@ -20,6 +9,7 @@ export const METHODS_CONFIG: Record<string, MethodConfig> = {
maxWindowLength: 48,
requiresState: false,
bufferOffset: 0,
supportedVitals: ['ppg_waveform', 'heart_rate'],
},
chrom: {
method: 'chrom',
Expand All @@ -29,6 +19,7 @@ export const METHODS_CONFIG: Record<string, MethodConfig> = {
maxWindowLength: 48,
requiresState: false,
bufferOffset: 0,
supportedVitals: ['ppg_waveform', 'heart_rate'],
},
g: {
method: 'g',
Expand All @@ -38,5 +29,6 @@ export const METHODS_CONFIG: Record<string, MethodConfig> = {
maxWindowLength: 64,
requiresState: false,
bufferOffset: 0,
supportedVitals: ['ppg_waveform', 'heart_rate'],
},
};
16 changes: 15 additions & 1 deletion src/core/VitalLens.base.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { VitalLensOptions, VitalLensResult, VideoInput } from '../types/core';
import {
VitalLensOptions,
VitalLensResult,
VideoInput,
Vital,
} from '../types/core';
import { IVitalLensController } from '../types/IVitalLensController';

/**
Expand Down Expand Up @@ -70,6 +75,15 @@ export abstract class VitalLensBase {
return this.controller.processVideoFile(videoInput);
}

/**
* Returns the vital signs supported by the currently resolved model.
* Must be called after the instance has initialized.
* @returns An array of supported vital sign keys.
*/
getSupportedVitals(): Vital[] {
return this.controller.getSupportedVitals();
}

/**
* Registers an event listener for a specific event.
* @param event - The event to listen to (e.g., 'vitals').
Expand Down
36 changes: 27 additions & 9 deletions src/core/VitalLensController.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
VitalLensResult,
VideoInput,
MethodConfig,
Vital,
} from '../types/core';
import { IVitalLensController } from '../types/IVitalLensController';
import { METHODS_CONFIG } from '../config/methodsConfig';
Expand Down Expand Up @@ -40,9 +41,11 @@ export abstract class VitalLensControllerBase implements IVitalLensController {
this.methodConfig = METHODS_CONFIG[this.options.method];
this.bufferManager = new BufferManager();
this.methodHandler = this.createMethodHandler(options);
this.frameIteratorFactory = new FrameIteratorFactory(options);
this.frameIteratorFactory = new FrameIteratorFactory(options, () =>
this.methodHandler.getConfig()
);
this.vitalsEstimateManager = new VitalsEstimateManager(
this.methodConfig,
() => this.methodHandler.getConfig(),
this.options,
this.methodHandler.postprocess.bind(this.methodHandler)
);
Expand Down Expand Up @@ -74,14 +77,15 @@ export abstract class VitalLensControllerBase implements IVitalLensController {
*/
protected abstract createStreamProcessor(
options: VitalLensOptions,
methodConfig: MethodConfig,
getConfig: () => MethodConfig,
frameIterator: IFrameIterator,
bufferManager: BufferManager,
faceDetectionWorker: IFaceDetectionWorker | null,
methodHandler: MethodHandler,
bufferedResultsConsumer: BufferedResultsConsumer | null,
onPredict: (result: VitalLensResult) => Promise<void>,
onNoFace: () => Promise<void>
onNoFace: () => Promise<void>,
onStreamReset: () => Promise<void>
): IStreamProcessor;

/**
Expand All @@ -91,7 +95,7 @@ export abstract class VitalLensControllerBase implements IVitalLensController {
*/
protected createMethodHandler(options: VitalLensOptions): MethodHandler {
if (
options.method === 'vitallens' &&
options.method.startsWith('vitallens') &&
!options.apiKey &&
!options.proxyUrl
) {
Expand All @@ -100,7 +104,7 @@ export abstract class VitalLensControllerBase implements IVitalLensController {
const requestMode = options.requestMode || 'rest'; // Default to REST
const dependencies = {
restClient:
options.method === 'vitallens' && requestMode === 'rest'
options.method.startsWith('vitallens') && requestMode === 'rest'
? this.createRestClient(
this.options.apiKey ?? '',
this.options.proxyUrl
Expand Down Expand Up @@ -143,7 +147,7 @@ export abstract class VitalLensControllerBase implements IVitalLensController {

this.streamProcessor = this.createStreamProcessor(
this.options,
this.methodConfig,
() => this.methodHandler.getConfig(),
frameIterator,
this.bufferManager,
this.faceDetectionWorker,
Expand Down Expand Up @@ -172,6 +176,12 @@ export abstract class VitalLensControllerBase implements IVitalLensController {
'vitals',
this.vitalsEstimateManager.getEmptyResult()
);
},
async () => {
// onStreamReset - dispatch a public event so the UI can react.
this.dispatchEvent('streamReset', {
message: 'Connection unstable. Stream is resetting.',
});
}
);
}
Expand Down Expand Up @@ -224,7 +234,6 @@ export abstract class VitalLensControllerBase implements IVitalLensController {

const frameIterator = this.frameIteratorFactory.createFileFrameIterator(
videoInput,
this.methodConfig,
this.ffmpeg,
this.faceDetectionWorker
);
Expand All @@ -251,7 +260,8 @@ export abstract class VitalLensControllerBase implements IVitalLensController {
const incrementalResult = await this.methodHandler.process(
framesChunk,
'file',
this.bufferManager.getState() ?? undefined
this.bufferManager.getState() ?? undefined,
undefined
);

if (incrementalResult) {
Expand Down Expand Up @@ -281,6 +291,14 @@ export abstract class VitalLensControllerBase implements IVitalLensController {
return result;
}

/**
* Returns the vital signs supported by the currently resolved model.
* @returns An array of supported vital sign keys.
*/
getSupportedVitals(): Vital[] {
return this.methodHandler.getConfig().supportedVitals || [];
}

/**
* Adds an event listener for a specific event.
* @param event - Event name (e.g., 'vitals').
Expand Down
Loading