From 2946fc6ce5e2fe282a60ea3a86397a8e6da60529 Mon Sep 17 00:00:00 2001 From: SaribAzim Date: Wed, 29 Jul 2026 23:55:03 +0500 Subject: [PATCH 1/4] instantiate BaselineCalibrator and wire it into the processing pipeline --- voiceMonitor/audio_stream.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/voiceMonitor/audio_stream.py b/voiceMonitor/audio_stream.py index 9e42d20..2ce6267 100644 --- a/voiceMonitor/audio_stream.py +++ b/voiceMonitor/audio_stream.py @@ -10,6 +10,7 @@ from .config import Config from .utils import timestamp, ensure_dir from .session import SessionReport +from .baseline import BaselineCalibrator logger = logging.getLogger(__name__) @@ -81,6 +82,7 @@ def __init__(self, threshold=None, chunk_dir=None): self.chunk_dir = chunk_dir or f"{Config.CHUNK_DIR}_{timestamp()}" ensure_dir(self.chunk_dir) self.session = SessionReport() + self.baseline = BaselineCalibrator() self._samples_processed = 0 def _save_chunk(self, audio_arr: np.ndarray, ts: str): @@ -102,8 +104,11 @@ def _process_chunk(self, wav_path: str, ts: str, elapsed_seconds: float): if Config.EXTRACT_ACOUSTIC_FEATURES: features = extract_acoustic_features(processed) + baseline_info = self.baseline.update(score, elapsed_seconds) + self.session.add_record( - ts, processed, score, elapsed_seconds, features=features + ts, processed, score, elapsed_seconds, + features=features, baseline_info=baseline_info, ) return score @@ -127,21 +132,13 @@ def start(self, duration_sec=None): if len(buffer) >= win_samples: ts = timestamp() - # elapsed_seconds is derived from the actual audio - # samples consumed so far, the same deterministic - # clock the audio stream itself runs on, rather than a - # separate wall clock that could drift due to - # processing latency between windows elapsed_seconds = self._samples_processed / Config.SAMPLE_RATE - # write raw to wav raw_file = self._save_chunk(buffer[:win_samples], ts) score = self._process_chunk(raw_file, ts, elapsed_seconds) - # slide buffer buffer = buffer[int(step_samples):] - # warnings & prints print(f"[{ts}] Score: {score:.2f}") if score >= self.threshold: print("⚠ fatigue threshold crossed") From 0a737dfc9e98d4df35996880284da2eee62ae807 Mon Sep 17 00:00:00 2001 From: SaribAzim Date: Wed, 29 Jul 2026 23:55:15 +0500 Subject: [PATCH 2/4] store baseline calibration info on session records --- voiceMonitor/session.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/voiceMonitor/session.py b/voiceMonitor/session.py index 3f783b0..e841a8f 100644 --- a/voiceMonitor/session.py +++ b/voiceMonitor/session.py @@ -6,7 +6,10 @@ def __init__(self): self.analytics = SessionAnalytics() self.records = [] - def add_record(self, timestamp, chunk_file, score, elapsed_seconds=None, features=None): + def add_record( + self, timestamp, chunk_file, score, elapsed_seconds=None, + features=None, baseline_info=None, + ): """ features: acoustic markers (jitter, shimmer, HNR, CPPS) collected alongside the primary auralis_vfs score. These are currently @@ -14,8 +17,15 @@ def add_record(self, timestamp, chunk_file, score, elapsed_seconds=None, feature influence the fatigue score, warning logic, or any analytics computed in this module. Integrating them into the fatigue model itself is planned as future work, not part of this change. + + baseline_info: output of BaselineCalibrator.update (baseline, + adjusted_score, is_provisional), stored alongside the raw score for + the same reason as features above. The raw score and warning + threshold logic are unchanged by this; baseline adjusted values are + exposed for inspection and future use, not yet wired into alerting. """ smoothed, impulse_state = self.analytics.add(score, timestamp, elapsed_seconds) + baseline_info = baseline_info or {} self.records.append({ "timestamp": timestamp, @@ -27,6 +37,9 @@ def add_record(self, timestamp, chunk_file, score, elapsed_seconds=None, feature "chronic_load": impulse_state["chronic"], "readiness_experimental": impulse_state["readiness_experimental"], "recovery_eta_sec": impulse_state["recovery_eta_sec"], + "baseline": baseline_info.get("baseline"), + "baseline_adjusted_score": baseline_info.get("adjusted_score"), + "baseline_is_provisional": baseline_info.get("is_provisional"), }) def export_json(self, path): From fcf93d69262d20b59c19fc9fe91f85564bdd9227 Mon Sep 17 00:00:00 2001 From: SaribAzim Date: Wed, 29 Jul 2026 23:55:28 +0500 Subject: [PATCH 3/4] add tests for baseline info storage on session records --- tests/test_session_report.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/test_session_report.py b/tests/test_session_report.py index 117a0a8..7a35009 100644 --- a/tests/test_session_report.py +++ b/tests/test_session_report.py @@ -91,4 +91,24 @@ def test_recovery_eta_decreases_as_session_recovers(): def test_recovery_eta_is_zero_for_low_scores(): report = SessionReport() report.add_record("ts1", "chunks/ts1.wav", 5, elapsed_seconds=0) - assert report.records[0]["recovery_eta_sec"] == 0 \ No newline at end of file + assert report.records[0]["recovery_eta_sec"] == 0 + +def test_add_record_stores_baseline_info_when_given(): + report = SessionReport() + baseline_info = {"baseline": 30, "adjusted_score": 20, "is_provisional": True} + report.add_record( + "ts1", "chunks/ts1.wav", 50, elapsed_seconds=0, baseline_info=baseline_info + ) + record = report.records[0] + assert record["baseline"] == 30 + assert record["baseline_adjusted_score"] == 20 + assert record["baseline_is_provisional"] is True + + +def test_add_record_baseline_fields_default_to_none_when_omitted(): + report = SessionReport() + report.add_record("ts1", "chunks/ts1.wav", 50, elapsed_seconds=0) + record = report.records[0] + assert record["baseline"] is None + assert record["baseline_adjusted_score"] is None + assert record["baseline_is_provisional"] is None \ No newline at end of file From ca6ef66890651d0ff4f79b4da662335bde8301c3 Mon Sep 17 00:00:00 2001 From: SaribAzim Date: Wed, 29 Jul 2026 23:55:44 +0500 Subject: [PATCH 4/4] add test verifying baseline calibration is wired into chunk processing --- tests/test_audio_stream.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/test_audio_stream.py b/tests/test_audio_stream.py index bd39c33..f1fb85b 100644 --- a/tests/test_audio_stream.py +++ b/tests/test_audio_stream.py @@ -86,4 +86,25 @@ def test_start_runs_for_fixed_duration_and_returns_session( assert session is vm.session # two one-second steps should have produced two processed windows assert len(session.records) == 2 - assert mock_score.call_count == 2 \ No newline at end of file + assert mock_score.call_count == 2 + +@patch("voiceMonitor.audio_stream.extract_acoustic_features") +@patch("voiceMonitor.audio_stream.score_audio") +@patch("voiceMonitor.audio_stream.preprocess_audio") +def test_process_chunk_wires_baseline_calibration( + mock_preprocess, mock_score, mock_features, tmp_path +): + mock_preprocess.return_value = ["processed_chunk.wav"] + mock_score.return_value = 50.0 + mock_features.return_value = {} + + vm = VoiceMonitor(chunk_dir=str(tmp_path / "chunks_test7")) + audio = np.zeros((Config.SAMPLE_RATE,), dtype="float32") + raw_file = vm._save_chunk(audio, "20260101_000000") + + vm._process_chunk(raw_file, "20260101_000000", elapsed_seconds=0) + + record = vm.session.records[0] + assert record["baseline"] is not None + assert record["baseline_adjusted_score"] is not None + assert record["baseline_is_provisional"] is True \ No newline at end of file