From b474f714aa7ae8c0c22f2345ce0dd0520a8936eb Mon Sep 17 00:00:00 2001 From: spapi Date: Mon, 16 Mar 2026 18:56:51 +0100 Subject: [PATCH 1/9] Remove unnecessary reprocessing of the last audio chunk --- simulstream/inference.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/simulstream/inference.py b/simulstream/inference.py index 9ec6210..1ee262c 100644 --- a/simulstream/inference.py +++ b/simulstream/inference.py @@ -53,14 +53,10 @@ def process_audio( # one speech chunk is the following samples_per_chunk = int( sample_rate * message_processor.speech_processor.speech_chunk_size) - i = 0 + for i in range(0, len(data), samples_per_chunk): output = message_processor.process_speech(data[i:i + samples_per_chunk].tobytes()) LOGGER.debug(f"response: {output}") - # send last part of the audio - if i < len(data): - output = message_processor.process_speech(data[i:].tobytes()) - LOGGER.debug(f"response: {output}") def run_inference( From dde4837137e54ba64287b5262f5ea7c7e44fe85f Mon Sep 17 00:00:00 2001 From: spapi Date: Mon, 16 Mar 2026 19:09:31 +0100 Subject: [PATCH 2/9] Add tests --- uts/test_inference.py | 96 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 uts/test_inference.py diff --git a/uts/test_inference.py b/uts/test_inference.py new file mode 100644 index 0000000..addc356 --- /dev/null +++ b/uts/test_inference.py @@ -0,0 +1,96 @@ +# Copyright 2026 FBK + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License + +import unittest +from unittest.mock import MagicMock, patch +import numpy as np + +from simulstream.inference import process_audio +from simulstream.server.message_processor import MessageProcessor +from simulstream.server.speech_processors import SAMPLE_RATE +from simulstream.server.speech_processors.incremental_output import IncrementalOutput + + +def make_speech_processor(chunk_size_seconds=1.0): + """Creates a mock SpeechProcessor with the minimal interface needed.""" + mock_output = IncrementalOutput( + new_tokens=[], deleted_tokens=0, new_string="", deleted_string="") + processor = MagicMock( + spec=["speech_chunk_size", "process_chunk", "end_of_stream","clear", "tokens_to_string"]) + processor.speech_chunk_size = chunk_size_seconds + processor.process_chunk.return_value = mock_output + processor.end_of_stream.return_value = mock_output + processor.tokens_to_string.return_value = "" + return processor + + +def make_message_processor(chunk_size_seconds=1.0): + speech_processor = make_speech_processor(chunk_size_seconds) + return MessageProcessor(client_id=0, speech_processor=speech_processor) + + +class TestProcessAudio(unittest.TestCase): + + def setUp(self): + # Suppress metrics logging side effects + patcher = patch("simulstream.server.message_processor.METRICS_LOGGER") + patcher.start() + self.addCleanup(patcher.stop) + + def test_exact_multiple(self): + chunk_size = 1.0 + message_processor = make_message_processor(chunk_size) + # 2 Full chunks, no reminder + data = np.zeros(SAMPLE_RATE * 2, dtype=np.int16) + + process_audio(message_processor, SAMPLE_RATE, data) + + self.assertEqual(message_processor.speech_processor.process_chunk.call_count, 2) + + def test_remainder_chunk_not_sent_twice(self): + chunk_size = 1.0 + message_processor = make_message_processor(chunk_size) + # 2 Full chunks + a remainder of 0.5s + data = np.zeros(int(SAMPLE_RATE * 2.5), dtype=np.int16) + + process_audio(message_processor, SAMPLE_RATE, data) + + # Process_chunk processes full chunks only; remainder stays buffered for end_of_stream + self.assertEqual(message_processor.speech_processor.process_chunk.call_count, 2) + self.assertGreater(len(message_processor.client_buffer), 0) + + def test_single_chunk(self): + chunk_size = 1.0 + message_processor = make_message_processor(chunk_size) + # Data smaller than one chunk (process_chunk not called, data stays buffered) + data = np.zeros(SAMPLE_RATE // 2, dtype=np.int16) # 0.5s + + process_audio(message_processor, SAMPLE_RATE, data) + + message_processor.speech_processor.process_chunk.assert_not_called() + self.assertGreater(len(message_processor.client_buffer), 0) + + def test_empty_data(self): + message_processor = make_message_processor() + # Empty array (process_chunk never called, buffer remains empty) + data = np.array([], dtype=np.int16) + + process_audio(message_processor, SAMPLE_RATE, data) + + message_processor.speech_processor.process_chunk.assert_not_called() + self.assertEqual(message_processor.client_buffer, b'') + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 4e0f8fbacf6b86b3b3476f5355703a7226c07850 Mon Sep 17 00:00:00 2001 From: spapi Date: Mon, 16 Mar 2026 19:14:38 +0100 Subject: [PATCH 3/9] Fix Lint --- uts/test_inference.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uts/test_inference.py b/uts/test_inference.py index addc356..e5239f3 100644 --- a/uts/test_inference.py +++ b/uts/test_inference.py @@ -27,7 +27,7 @@ def make_speech_processor(chunk_size_seconds=1.0): mock_output = IncrementalOutput( new_tokens=[], deleted_tokens=0, new_string="", deleted_string="") processor = MagicMock( - spec=["speech_chunk_size", "process_chunk", "end_of_stream","clear", "tokens_to_string"]) + spec=["speech_chunk_size", "process_chunk", "end_of_stream", "clear", "tokens_to_string"]) processor.speech_chunk_size = chunk_size_seconds processor.process_chunk.return_value = mock_output processor.end_of_stream.return_value = mock_output @@ -93,4 +93,4 @@ def test_empty_data(self): if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() From eef73a239cbdd6037f5a47b5b7c9cc9df98d3379 Mon Sep 17 00:00:00 2001 From: sarapapi <57095209+sarapapi@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:13:35 +0100 Subject: [PATCH 4/9] Apply suggestions from code review Co-authored-by: Marco Gaido --- uts/test_inference.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uts/test_inference.py b/uts/test_inference.py index e5239f3..be0b166 100644 --- a/uts/test_inference.py +++ b/uts/test_inference.py @@ -68,7 +68,7 @@ def test_remainder_chunk_not_sent_twice(self): # Process_chunk processes full chunks only; remainder stays buffered for end_of_stream self.assertEqual(message_processor.speech_processor.process_chunk.call_count, 2) - self.assertGreater(len(message_processor.client_buffer), 0) + self.assertEqual(len(message_processor.client_buffer), SAMPLE_RATE * 0.5) def test_single_chunk(self): chunk_size = 1.0 @@ -79,7 +79,7 @@ def test_single_chunk(self): process_audio(message_processor, SAMPLE_RATE, data) message_processor.speech_processor.process_chunk.assert_not_called() - self.assertGreater(len(message_processor.client_buffer), 0) + self.assertEqual(len(message_processor.client_buffer), SAMPLE_RATE * 0.5) def test_empty_data(self): message_processor = make_message_processor() From 1ec898682fdb4df79d6b998de52f398c121f0424 Mon Sep 17 00:00:00 2001 From: spapi Date: Tue, 17 Mar 2026 16:20:11 +0100 Subject: [PATCH 5/9] Add check empty buffer with exact multiple --- uts/test_inference.py | 1 + 1 file changed, 1 insertion(+) diff --git a/uts/test_inference.py b/uts/test_inference.py index be0b166..01f77df 100644 --- a/uts/test_inference.py +++ b/uts/test_inference.py @@ -57,6 +57,7 @@ def test_exact_multiple(self): process_audio(message_processor, SAMPLE_RATE, data) self.assertEqual(message_processor.speech_processor.process_chunk.call_count, 2) + self.assertEqual(message_processor.client_buffer, b'') def test_remainder_chunk_not_sent_twice(self): chunk_size = 1.0 From b72059cbebc4ae1b330c8ce8498e0492b56eea79 Mon Sep 17 00:00:00 2001 From: spapi Date: Tue, 17 Mar 2026 16:23:30 +0100 Subject: [PATCH 6/9] Correct casting in tests --- uts/test_inference.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uts/test_inference.py b/uts/test_inference.py index 01f77df..7a33d64 100644 --- a/uts/test_inference.py +++ b/uts/test_inference.py @@ -69,7 +69,7 @@ def test_remainder_chunk_not_sent_twice(self): # Process_chunk processes full chunks only; remainder stays buffered for end_of_stream self.assertEqual(message_processor.speech_processor.process_chunk.call_count, 2) - self.assertEqual(len(message_processor.client_buffer), SAMPLE_RATE * 0.5) + self.assertEqual(len(message_processor.client_buffer), int(SAMPLE_RATE * 0.5)) def test_single_chunk(self): chunk_size = 1.0 @@ -80,7 +80,7 @@ def test_single_chunk(self): process_audio(message_processor, SAMPLE_RATE, data) message_processor.speech_processor.process_chunk.assert_not_called() - self.assertEqual(len(message_processor.client_buffer), SAMPLE_RATE * 0.5) + self.assertEqual(len(message_processor.client_buffer), int(SAMPLE_RATE * 0.5)) def test_empty_data(self): message_processor = make_message_processor() From 69a041888d107a32cf40ac973e7e3ac1b6102297 Mon Sep 17 00:00:00 2001 From: spapi Date: Tue, 17 Mar 2026 16:28:34 +0100 Subject: [PATCH 7/9] Correct casting in tests --- uts/test_inference.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/uts/test_inference.py b/uts/test_inference.py index 7a33d64..0489f8a 100644 --- a/uts/test_inference.py +++ b/uts/test_inference.py @@ -69,7 +69,8 @@ def test_remainder_chunk_not_sent_twice(self): # Process_chunk processes full chunks only; remainder stays buffered for end_of_stream self.assertEqual(message_processor.speech_processor.process_chunk.call_count, 2) - self.assertEqual(len(message_processor.client_buffer), int(SAMPLE_RATE * 0.5)) + # Each sample is int16 (2 bytes), so the buffer size in bytes is samples * 2 + self.assertEqual(len(message_processor.client_buffer), int(SAMPLE_RATE * 0.5) * 2) def test_single_chunk(self): chunk_size = 1.0 @@ -80,7 +81,8 @@ def test_single_chunk(self): process_audio(message_processor, SAMPLE_RATE, data) message_processor.speech_processor.process_chunk.assert_not_called() - self.assertEqual(len(message_processor.client_buffer), int(SAMPLE_RATE * 0.5)) + # Each sample is int16 (2 bytes), so the buffer size in bytes is samples * 2 + self.assertEqual(len(message_processor.client_buffer), int(SAMPLE_RATE * 0.5) * 2) def test_empty_data(self): message_processor = make_message_processor() From b1788d91b4722af500f2fe4d8f059f1edefd39a6 Mon Sep 17 00:00:00 2001 From: spapi Date: Tue, 17 Mar 2026 18:26:17 +0100 Subject: [PATCH 8/9] Remove logging patch --- uts/test_inference.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/uts/test_inference.py b/uts/test_inference.py index 0489f8a..4a80329 100644 --- a/uts/test_inference.py +++ b/uts/test_inference.py @@ -42,12 +42,6 @@ def make_message_processor(chunk_size_seconds=1.0): class TestProcessAudio(unittest.TestCase): - def setUp(self): - # Suppress metrics logging side effects - patcher = patch("simulstream.server.message_processor.METRICS_LOGGER") - patcher.start() - self.addCleanup(patcher.stop) - def test_exact_multiple(self): chunk_size = 1.0 message_processor = make_message_processor(chunk_size) From f8a027a7fa7a9170763b6d9a0aa99950a72c9ecd Mon Sep 17 00:00:00 2001 From: spapi Date: Tue, 17 Mar 2026 18:30:21 +0100 Subject: [PATCH 9/9] Remove patch import from unittest --- uts/test_inference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uts/test_inference.py b/uts/test_inference.py index 4a80329..7a4b458 100644 --- a/uts/test_inference.py +++ b/uts/test_inference.py @@ -13,7 +13,7 @@ # limitations under the License import unittest -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock import numpy as np from simulstream.inference import process_audio