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:
+18
-27
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user