fix(dictation): remove unwanted leading space when starting new transcription (#307)

Reset last_injected_text when recognition transitions to LISTENING state,
ensuring the first dictation segment in each session is not prefixed with
a space. Also simplify the space-prepending logic: always add a separator
space between consecutive segments (not just after punctuation), and strip
incoming text as a safety net against whisper tokenizer artifacts.
This commit is contained in:
Jatin K Malik
2026-03-14 12:40:58 -07:00
committed by GitHub
parent 80108c3456
commit e35bdb2f62
2 changed files with 88 additions and 27 deletions
+18 -27
View File
@@ -177,6 +177,7 @@ def main():
sys.exit(1)
# Now it's safe to import GTK-dependent modules
from .common_types import RecognitionState
from .speech_recognition import recognition_manager
from .text_injection import text_injector
from .ui import tray_indicator
@@ -292,35 +293,19 @@ def main():
# Create a wrapper function to track injected text for action handler
def text_callback_wrapper(text: str):
"""Wrapper to track injected text and handle it."""
# Check if we need to add a space before the new text
text_to_inject = text
"""Wrapper to track injected text and handle spacing between segments."""
# Strip any leading/trailing whitespace from the incoming text as a
# safety net (whisper tokenizer sometimes prepends spaces to tokens)
text_to_inject = text.strip()
if not text_to_inject:
return
# Punctuation and characters that typically need a space after them
chars_needing_space = {
".",
",",
"!",
"?",
";",
":",
")",
"]",
"}",
"-",
"_",
# Also quotes
'"',
"'",
}
# If last injected text exists and ends with a character that needs space,
# add a space before the new text
# Add a separating space between consecutive dictation segments,
# but never for the very first segment (avoids unwanted leading space
# when starting dictation in an empty text field).
if action_handler.last_injected_text and action_handler.last_injected_text.strip():
last_char = action_handler.last_injected_text[-1]
if last_char in chars_needing_space:
text_to_inject = " " + text_to_inject
logger.debug(f"Added space before new text (last char: '{last_char}')")
text_to_inject = " " + text_to_inject
logger.debug("Added space separator before new segment")
success = text_system.inject_text(text_to_inject)
if success:
@@ -330,6 +315,12 @@ def main():
speech_engine.register_text_callback(text_callback_wrapper)
speech_engine.register_action_callback(action_handler.handle_action)
def on_state_change(state: RecognitionState):
if state == RecognitionState.LISTENING:
action_handler.set_last_injected_text("")
speech_engine.register_state_callback(on_state_change)
# Initialize and start the system tray indicator
indicator = tray_indicator.TrayIndicator(
speech_engine=speech_engine,
+70
View File
@@ -233,6 +233,7 @@ class TestMainModule(unittest.TestCase):
mock_speech_instance.register_action_callback.assert_called_once_with(
mock_action_instance.handle_action
)
mock_speech_instance.register_state_callback.assert_called_once()
# Verify the tray indicator was started
mock_tray_instance.run.assert_called_once()
@@ -648,5 +649,74 @@ class TestMainConfigPrecedence(unittest.TestCase):
self.assertEqual(call_kwargs["audio_device_index"], 2)
class TestTextCallbackSpacing(unittest.TestCase):
"""Test spacing logic in text_callback_wrapper."""
def _make_callback(self):
"""Build text_callback_wrapper with mocked dependencies."""
from vocalinux.ui.action_handler import ActionHandler
text_system = MagicMock()
text_system.inject_text.return_value = True
action_handler = ActionHandler(text_system)
def text_callback_wrapper(text: str):
text_to_inject = text.strip()
if not text_to_inject:
return
if action_handler.last_injected_text and action_handler.last_injected_text.strip():
text_to_inject = " " + text_to_inject
success = text_system.inject_text(text_to_inject)
if success:
action_handler.set_last_injected_text(text)
return text_callback_wrapper, text_system, action_handler
def test_first_segment_has_no_leading_space(self):
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("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("first session")
ah.set_last_injected_text("")
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.inject_text.assert_not_called()
def test_input_with_leading_space_is_stripped(self):
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("one")
cb("two")
cb("three")
calls = [c.args[0] for c in text_system.inject_text.call_args_list]
self.assertEqual(calls, ["one", " two", " three"])
def test_space_after_punctuation_segment(self):
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"])
if __name__ == "__main__":
unittest.main()