From 1cdb8e88f869bfde0b2b30413d7f03bc795fcc0e Mon Sep 17 00:00:00 2001 From: Philipp Rouast Date: Sat, 31 Oct 2026 13:39:43 +1100 Subject: [PATCH 1/6] Clean up CONTRIBUTING.md, use time.monotonic() in the stream() docstring --- CONTRIBUTING.md | 6 ------ vitallens/client.py | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0169e0c..32bb9c9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,12 +67,6 @@ python -m build The artifacts will be generated in the `dist/` directory. -Since this is a Python project using `setuptools_scm`, versioning is handled automatically based on git tags. I've adapted the workflow from your JS library to Python conventions while maintaining the same straightforward structure. - -Add this section to the end of your `CONTRIBUTING.md`: - ---- - ## Releases Releases are automated via GitHub Actions and triggered by pushing git tags. The project uses `setuptools_scm`, so the version is derived directly from the tag name. diff --git a/vitallens/client.py b/vitallens/client.py index 94ca557..984a1d8 100644 --- a/vitallens/client.py +++ b/vitallens/client.py @@ -271,7 +271,7 @@ def stream(self, on_result: Callable = None) -> StreamSession: Returns: session: A `StreamSession` context manager. The session object provides: * `push(frame, timestamp, face=None)`: Ingests a new RGB frame. - `timestamp` should be in seconds (e.g., `time.time()`). + `timestamp` is the capture time in seconds (e.g., `time.monotonic()`). * `get_result(block=False, timeout=None)`: Pulls the latest analysis results from the queue. * `current_face`: The most recently detected face coordinates. From 0ad577612090ec3e1c49c5a4cde3b72794902f14 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 09:05:33 +0000 Subject: [PATCH 2/6] Bump softprops/action-gh-release from 2 to 3 Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 085b9a8..be3318f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,7 +36,7 @@ jobs: uses: pypa/gh-action-pypi-publish@release/v1 - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: files: dist/* generate_release_notes: true From 9f0739601773d1467d88e09172d2afb408e6a022 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 09:05:46 +0000 Subject: [PATCH 3/6] Bump actions/setup-python from 6 to 7 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/main.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 71be36f..f5dab19 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,7 +22,7 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - name: Cache pip dependencies @@ -71,7 +71,7 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} - name: Set up Python 3.11 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.11" - name: Cache pip dependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 085b9a8..db2742a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.11" From 34a2696509cf5e11adb5130db7737fea8361a605 Mon Sep 17 00:00:00 2001 From: Philipp Rouast Date: Tue, 3 Nov 2026 11:33:09 +1100 Subject: [PATCH 4/6] Results: add core_version and model_used; vitallens-core 0.4.2 --- docs/results.md | 2 +- pyproject.toml | 2 +- tests/test_client.py | 3 +++ tests/test_stream.py | 3 ++- tests/test_vitallens.py | 1 + vitallens/client.py | 6 +++++- vitallens/methods/vitallens.py | 3 +++ vitallens/stream.py | 2 ++ 8 files changed, 18 insertions(+), 4 deletions(-) diff --git a/docs/results.md b/docs/results.md index c4eb0fd..ddf87ac 100644 --- a/docs/results.md +++ b/docs/results.md @@ -14,7 +14,7 @@ hr = results[0]['vitals']['heart_rate']['value'] Waveform data, confidences and timestamps are returned as lists; scalar values are Python floats. -Besides `face`, `vitals`, `waveforms` and `message`, each result contains: +Besides `face`, `vitals`, `waveforms`, `message`, `model_used` (VitalLens API only) and `core_version`, each result contains: | Key | Contents | | --- | --- | diff --git a/pyproject.toml b/pyproject.toml index aed82c1..2403932 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "python-dotenv>=1.0", "pyyaml>=6.0.1", "requests>=2.32.0", - "vitallens-core==0.4.0", + "vitallens-core==0.4.2", ] dynamic = ["version"] diff --git a/tests/test_client.py b/tests/test_client.py index 84ba756..dca85c0 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,6 +8,7 @@ import sys sys.path.append('../vitallens-python') +import vitallens_core from vitallens.client import VitalLens, Method @pytest.mark.parametrize("method_arg", [Method.G, "chrom", "pos"]) @@ -30,6 +31,8 @@ def test_VitalLens(request, method_arg, detect_faces, file, export): assert np.asarray(result[0]['waveforms']['ppg_waveform']['confidence']).shape == (630,) np.testing.assert_allclose(result[0]['vitals']['heart_rate']['value'], 60, atol=10) assert result[0]['vitals']['heart_rate']['confidence'] == 1.0 + assert result[0]['core_version'] == vitallens_core.core_version() + assert 'model_used' not in result[0] if export: test_json_path = os.path.join("test.json") assert os.path.exists(test_json_path) diff --git a/tests/test_stream.py b/tests/test_stream.py index d3cc8b9..f7750e1 100644 --- a/tests/test_stream.py +++ b/tests/test_stream.py @@ -74,4 +74,5 @@ def test_stream_session(): assert len(res) > 0 assert 'vitals' in res[0] assert 'waveforms' in res[0] - assert 'ppg_waveform' in res[0]['waveforms'] \ No newline at end of file + assert 'ppg_waveform' in res[0]['waveforms'] + assert res[0]['core_version'] == vc.core_version() \ No newline at end of file diff --git a/tests/test_vitallens.py b/tests/test_vitallens.py index bd07b7d..93be4c9 100644 --- a/tests/test_vitallens.py +++ b/tests/test_vitallens.py @@ -152,6 +152,7 @@ def test_VitalLensRPPGMethod_file_mock(mock_post, mock_resolve_config, request, assert data['respiratory_waveform'].shape == (test_video_ndarray.shape[0],) assert conf['respiratory_waveform'].shape == (test_video_ndarray.shape[0],) assert live.shape == (test_video_ndarray.shape[0],) + assert method.model_used == requested_model @pytest.mark.parametrize("override_fps_target", [None, 15, 10]) @pytest.mark.parametrize("long", [True]) diff --git a/vitallens/client.py b/vitallens/client.py index 984a1d8..a1cc191 100644 --- a/vitallens/client.py +++ b/vitallens/client.py @@ -167,7 +167,9 @@ def __call__( }, }, - 'message': + 'message': , + 'model_used': 'vitallens-2.0', + 'core_version': '0.4.2' }, { @@ -234,6 +236,8 @@ def __call__( for k, v in session_result.rolling_vitals.items() } if session_result.rolling_vitals else {}, 'message': session_result.message, + **({'model_used': self.rppg.model_used} if getattr(self.rppg, 'model_used', None) else {}), + 'core_version': vc.core_version(), 'fps': session_result.fps, 'n': len(session_result.timestamp), 'time': session_result.timestamp diff --git a/vitallens/methods/vitallens.py b/vitallens/methods/vitallens.py index 275b8fc..d4f2424 100644 --- a/vitallens/methods/vitallens.py +++ b/vitallens/methods/vitallens.py @@ -115,6 +115,7 @@ def __init__( self.session_config = _resolve_model_config(api_key=api_key, model_name=requested_model_name, proxies=proxies) self.parse_config(self.session_config) self.resolved_model = self.session_config.model_name + self.model_used = None self.requested_model_name = requested_model_name if requested_model_name != "vitallens" else None self.http_session = requests.Session() @@ -270,6 +271,7 @@ def infer_stream(self, frames: np.ndarray, fps: float, state=None): else: raise VitalLensAPIError(f"API Error: {response_body.get('message')}") response_body = response.json() + self.model_used = response_body.get('model_used', self.model_used) api_waveforms = response_body.get("waveforms", {}) sig_dict, conf_dict = {}, {} @@ -378,6 +380,7 @@ def process_api_batch( raise VitalLensAPIError(f"API Error: {response_body['message']}") else: raise Exception(f"Error {response.status_code}: {response_body['message']}") + self.model_used = response_body.get('model_used', self.model_used) # Dynamic dict parsing api_waveforms = response_body.get("waveforms", {}) sig_ds = {} diff --git a/vitallens/stream.py b/vitallens/stream.py index 1ad1d0a..439d5c5 100644 --- a/vitallens/stream.py +++ b/vitallens/stream.py @@ -399,6 +399,8 @@ def _format_result(self, session_result): 'vitals': {}, 'waveforms': {}, 'message': session_result.message, + **({'model_used': self.rppg.model_used} if getattr(self.rppg, 'model_used', None) else {}), + 'core_version': vc.core_version(), 'fps': session_result.fps, 'n': len(session_result.timestamp), 'time': session_result.timestamp From add093a459dfe5bd631edc944f04bbcd9daa0bd2 Mon Sep 17 00:00:00 2001 From: Philipp Rouast Date: Tue, 3 Nov 2026 11:39:04 +1100 Subject: [PATCH 5/6] =?UTF-8?q?API=20test:=20HRV=20expectations=20on=20sam?= =?UTF-8?q?ple=5Fvideo=5F2=20from=20measured=20values=20(SDNN=2060,=20RMSS?= =?UTF-8?q?D=2057,=20=C2=B110=20ms)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index dca85c0..0e8edc8 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -71,8 +71,8 @@ def test_VitalLens_API(request): np.testing.assert_allclose(result[0]['vitals']['respiratory_rate']['confidence'], 1.0, atol=0.1) supports_hrv = result[0].get('model_used', '') == 'vitallens-2.0' if supports_hrv: - np.testing.assert_allclose(result[0]['vitals']['hrv_sdnn']['value'], 40, atol=20) - np.testing.assert_allclose(result[0]['vitals']['hrv_rmssd']['value'], 30, atol=20) + np.testing.assert_allclose(result[0]['vitals']['hrv_sdnn']['value'], 60, atol=10) + np.testing.assert_allclose(result[0]['vitals']['hrv_rmssd']['value'], 57, atol=10) assert not os.path.exists("test.json") @pytest.mark.integration From 77ea43587cfb0179284a8aa5d211d76844198071 Mon Sep 17 00:00:00 2001 From: Philipp Rouast Date: Tue, 3 Nov 2026 11:43:49 +1100 Subject: [PATCH 6/6] API test: HR and HRV expectations from the contact PPG of sample_video_2 (HR 60.4, SDNN 62, RMSSD 54) --- tests/test_client.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 0e8edc8..bf10a69 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -65,14 +65,15 @@ def test_VitalLens_API(request): assert np.asarray(result[0]['waveforms']['respiratory_waveform']['confidence']).shape == (630,) assert 'hrv_sdnn' in result[0]['vitals'] assert 'hrv_rmssd' in result[0]['vitals'] - np.testing.assert_allclose(result[0]['vitals']['heart_rate']['value'], 60, atol=0.5) + # HR and HRV reference values from the contact PPG in examples/sample_vitals_2.csv + np.testing.assert_allclose(result[0]['vitals']['heart_rate']['value'], 60.4, atol=0.5) np.testing.assert_allclose(result[0]['vitals']['heart_rate']['confidence'], 1.0, atol=0.1) np.testing.assert_allclose(result[0]['vitals']['respiratory_rate']['value'], 13, atol=1.0) np.testing.assert_allclose(result[0]['vitals']['respiratory_rate']['confidence'], 1.0, atol=0.1) supports_hrv = result[0].get('model_used', '') == 'vitallens-2.0' if supports_hrv: - np.testing.assert_allclose(result[0]['vitals']['hrv_sdnn']['value'], 60, atol=10) - np.testing.assert_allclose(result[0]['vitals']['hrv_rmssd']['value'], 57, atol=10) + np.testing.assert_allclose(result[0]['vitals']['hrv_sdnn']['value'], 62, atol=10) + np.testing.assert_allclose(result[0]['vitals']['hrv_rmssd']['value'], 54, atol=10) assert not os.path.exists("test.json") @pytest.mark.integration