fix(dictation): preserve spacing between silence segments (#464)

This commit is contained in:
Jatin K Malik
2026-06-10 15:44:27 -07:00
committed by GitHub
parent 748d318515
commit a73eec4a0a
2 changed files with 91 additions and 15 deletions
+5 -5
View File
@@ -409,8 +409,8 @@ def main():
#
# state_callback(state: RecognitionState)
# Called whenever the engine transitions state (IDLE → LISTENING,
# etc.). Used here to clear the "last injected" buffer when a
# new listening session starts.
# etc.). Used here to clear the "last injected" buffer after a
# listening session ends.
# ------------------------------------------------------------------
def text_callback_wrapper(text: str) -> None:
@@ -437,11 +437,11 @@ def main():
success = text_system.inject_text(text_to_inject)
if success:
action_handler.set_last_injected_text(text)
action_handler.set_last_injected_text(text_to_inject)
def on_state_change(state: RecognitionState) -> None:
"""Reset the last-injected buffer when a new listening session starts."""
if state == RecognitionState.LISTENING:
"""Reset the last-injected buffer when a listening session ends."""
if state == RecognitionState.IDLE:
action_handler.set_last_injected_text("")
# Connect speech recognition to text injection and action handling
+86 -10
View File
@@ -12,6 +12,7 @@ sys.modules["gi"] = MagicMock()
sys.modules["gi.repository"] = MagicMock()
# Update import to use the new package structure
from vocalinux.common_types import RecognitionState
from vocalinux.main import check_dependencies, main, parse_arguments
@@ -251,6 +252,68 @@ class TestMainModule(unittest.TestCase):
# Verify the tray indicator was started
mock_tray_instance.run.assert_called_once()
@patch("vocalinux.main.check_dependencies")
@patch("vocalinux.speech_recognition.recognition_manager.SpeechRecognitionManager")
@patch("vocalinux.text_injection.text_injector.TextInjector")
@patch("vocalinux.ui.tray_indicator.TrayIndicator")
@patch("vocalinux.ui.config_manager.ConfigManager")
@patch("vocalinux.ui.logging_manager.initialize_logging")
def test_registered_callbacks_keep_spacing_across_processing_to_listening(
self,
mock_init_logging,
mock_config_manager,
mock_tray,
mock_text,
mock_speech,
mock_check_deps,
):
"""Test main callback wiring preserves spacing between in-session segments."""
mock_check_deps.return_value = True
mock_config_instance = MagicMock()
mock_config_instance.get_settings.return_value = {
"speech_recognition": {},
"general": {"first_run": False},
}
mock_config_manager.return_value = mock_config_instance
mock_speech_instance = MagicMock()
mock_text_instance = MagicMock()
mock_text_instance.inject_text.return_value = True
mock_tray_instance = MagicMock()
mock_speech.return_value = mock_speech_instance
mock_text.return_value = mock_text_instance
mock_tray.return_value = mock_tray_instance
with patch("vocalinux.main.parse_arguments") as mock_parse:
mock_args = MagicMock()
mock_args.debug = False
mock_args.model = "medium"
mock_args.engine = "vosk"
mock_args.language = "en-us"
mock_args.wayland = False
mock_args.start_minimized = False
mock_parse.return_value = mock_args
main()
text_callback = mock_speech_instance.register_text_callback.call_args.args[0]
state_callback = mock_speech_instance.register_state_callback.call_args.args[0]
text_callback("Hello.")
state_callback(RecognitionState.PROCESSING)
state_callback(RecognitionState.LISTENING)
text_callback("World")
calls = [call.args[0] for call in mock_text_instance.inject_text.call_args_list]
self.assertEqual(calls, ["Hello.", " World"])
state_callback(RecognitionState.IDLE)
mock_text_instance.inject_text.reset_mock()
text_callback("Next session")
mock_text_instance.inject_text.assert_called_once_with("Next session")
@patch("vocalinux.main.check_dependencies")
@patch("vocalinux.ui.action_handler.ActionHandler")
@patch("vocalinux.speech_recognition.recognition_manager.SpeechRecognitionManager")
@@ -700,42 +763,46 @@ class TestTextCallbackSpacing(unittest.TestCase):
text_to_inject = " " + text_to_inject
success = text_system.inject_text(text_to_inject)
if success:
action_handler.set_last_injected_text(text)
action_handler.set_last_injected_text(text_to_inject)
return text_callback_wrapper, text_system, action_handler
def on_state_change(state: RecognitionState):
if state == RecognitionState.IDLE:
action_handler.set_last_injected_text("")
return text_callback_wrapper, on_state_change, text_system, action_handler
def test_first_segment_has_no_leading_space(self):
cb, text_system, _ = self._make_callback()
cb, _, text_system, _ = self._make_callback()
cb("Hello world")
text_system.inject_text.assert_called_once_with("Hello world")
def test_subsequent_segment_gets_space_separator(self):
cb, text_system, _ = self._make_callback()
cb, _, text_system, _ = self._make_callback()
cb("Hello")
cb("world")
calls = [c.args[0] for c in text_system.inject_text.call_args_list]
self.assertEqual(calls, ["Hello", " world"])
def test_reset_clears_leading_space(self):
cb, text_system, ah = self._make_callback()
cb, on_state_change, text_system, _ = self._make_callback()
cb("first session")
ah.set_last_injected_text("")
on_state_change(RecognitionState.IDLE)
text_system.inject_text.reset_mock()
cb("second session")
text_system.inject_text.assert_called_once_with("second session")
def test_whitespace_only_input_is_skipped(self):
cb, text_system, _ = self._make_callback()
cb, _, text_system, _ = self._make_callback()
cb(" ")
text_system.inject_text.assert_not_called()
def test_input_with_leading_space_is_stripped(self):
cb, text_system, _ = self._make_callback()
cb, _, text_system, _ = self._make_callback()
cb(" Hello world")
text_system.inject_text.assert_called_once_with("Hello world")
def test_multiple_segments_all_get_separators(self):
cb, text_system, _ = self._make_callback()
cb, _, text_system, _ = self._make_callback()
cb("one")
cb("two")
cb("three")
@@ -743,12 +810,21 @@ class TestTextCallbackSpacing(unittest.TestCase):
self.assertEqual(calls, ["one", " two", " three"])
def test_space_after_punctuation_segment(self):
cb, text_system, _ = self._make_callback()
cb, _, text_system, _ = self._make_callback()
cb("Hello.")
cb("World")
calls = [c.args[0] for c in text_system.inject_text.call_args_list]
self.assertEqual(calls, ["Hello.", " World"])
def test_processing_to_listening_keeps_segment_spacing(self):
cb, on_state_change, text_system, _ = self._make_callback()
cb("Hello.")
on_state_change(RecognitionState.PROCESSING)
on_state_change(RecognitionState.LISTENING)
cb("World")
calls = [c.args[0] for c in text_system.inject_text.call_args_list]
self.assertEqual(calls, ["Hello.", " World"])
if __name__ == "__main__":
unittest.main()