fix(commands): keep line breaks from standalone voice commands (#958)
* fix(commands): keep line breaks from standalone voice commands
Two stacked defects ate the newline "new line" / "new paragraph"
produce when spoken alone:
* _normalize_segment_text stripped all leading whitespace, so the
command's leading newline never reached the injector (a clean
"new line" segment was dropped entirely).
* whisper.cpp transcripts arrive punctuated ("New line."), which
missed the exact-match ladder and left a stray "." after the
substituted break, injected as ". ".
Normalize now strips only spaces/tabs, and the generic path
substitutes on a punctuation-stripped core when the whole segment
is command phrases, so engine edge punctuation cannot leak while
real text keeps it.
Fixes #955
* fix(injection): deliver newline-only segments to the focused app
inject_text treated whitespace-only text as empty and returned early,
so the "\n" a standalone "new line" now correctly produces was
discarded before any backend ran ("Empty text provided, skipping
injection"). Only a truly empty segment is skipped; whitespace-only
input is typed/pasted verbatim (xdotool types "\n" as Return).
Space/tab-only segments never reach this point: segment normalization
strips them upstream.
Fixes #955
* fix(injection): keep skipping space/tab-only segments
The previous commit let every whitespace-only string through, which
also delivered bare spaces and tabs a caller never wants typed.
Narrow the skip to text with no content outside space/tab so "\n"
still reaches the backend while " " stays a no-op (the pre-existing
test_empty_text_injection contract).
Fixes #955
* fix(injection): deliver newline segments over IBus too
IBusTextInjector.inject_text had the same whitespace guard and dropped
"\n" before it reached the engine socket; narrow it the same way so
a newline-only segment is sent while space/tab-only stays a no-op.
Also let capitalize_sentences uppercase the first letter after leading
whitespace, so Vosk + auto_capitalize keeps working on "\n hello".
Covers Greptile's findings: real-socket test for the IBus path, the
capitalize regression via the main() callback seam, and type hints on
the new injector tests.
Fixes #955
* test(injection): add type hints to new IBus socket test
AGENTS.md requires hints on every signature; the Greptile review
flagged the new test for missing them.
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
This commit is contained in:
co-authored by
Jatin K Malik
parent
64032fb999
commit
0ddb127437
@@ -512,3 +512,83 @@ class TestCommandPhrases(unittest.TestCase):
|
||||
self.assertEqual(aliases[f"point d{apostrophe}interrogation"], "?")
|
||||
self.assertEqual(aliases[f"point d{apostrophe}exclamation"], "!")
|
||||
self.assertEqual(aliases["point"], ".")
|
||||
|
||||
|
||||
class TestStandaloneCommandPunctuation(unittest.TestCase):
|
||||
"""Engine sentence-final punctuation on standalone commands (issue #955).
|
||||
|
||||
whisper.cpp transcripts arrive capitalized and punctuated ("New line."),
|
||||
which used to leave a stray "." after the substituted newline — or, for
|
||||
punctuation commands, a doubled mark. Edge punctuation is dropped only
|
||||
when the whole segment is command phrases; real text keeps it.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Set up for tests."""
|
||||
self.processor = CommandProcessor()
|
||||
|
||||
def test_standalone_line_breaks_with_engine_punctuation(self) -> None:
|
||||
"""Punctuated standalone line-break commands still emit the break."""
|
||||
cases = [
|
||||
("New line.", "\n"),
|
||||
("new line.", "\n"),
|
||||
("New line!", "\n"),
|
||||
("New line?", "\n"),
|
||||
("new line...", "\n"),
|
||||
(", new line", "\n"),
|
||||
("New paragraph.", "\n\n"),
|
||||
("new paragraph!", "\n\n"),
|
||||
]
|
||||
for input_text, expected in cases:
|
||||
with self.subTest(input_text=input_text):
|
||||
result, actions = self.processor.process_text(input_text)
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(actions, [])
|
||||
|
||||
def test_standalone_punctuation_commands_with_engine_punctuation(self) -> None:
|
||||
"""Punctuated standalone punctuation commands emit one mark, not two."""
|
||||
cases = [
|
||||
("Period.", "."),
|
||||
("Full stop.", "."),
|
||||
("Comma.", ","),
|
||||
("Question mark?", "?"),
|
||||
]
|
||||
for input_text, expected in cases:
|
||||
with self.subTest(input_text=input_text):
|
||||
result, actions = self.processor.process_text(input_text)
|
||||
self.assertEqual(result, expected)
|
||||
self.assertEqual(actions, [])
|
||||
|
||||
def test_standalone_action_commands_with_engine_punctuation(self) -> None:
|
||||
"""Punctuated standalone action commands fire without stray text."""
|
||||
cases = [
|
||||
("Delete that.", ["delete_last"]),
|
||||
("Select all.", ["select_all"]),
|
||||
("Undo!", ["undo"]),
|
||||
("Redo?", ["redo"]),
|
||||
]
|
||||
for input_text, expected_actions in cases:
|
||||
with self.subTest(input_text=input_text):
|
||||
result, actions = self.processor.process_text(input_text)
|
||||
self.assertEqual(result, "")
|
||||
self.assertEqual(actions, expected_actions)
|
||||
|
||||
def test_real_text_keeps_engine_sentence_punctuation(self) -> None:
|
||||
"""Non-command utterances keep the punctuation the engine emitted."""
|
||||
cases = [
|
||||
("Hello world.", "Hello world.", []),
|
||||
("capitalize word.", "Word.", []),
|
||||
("the end full stop", "the end.", []),
|
||||
("scratch that previous text.", "previous text.", ["delete_last"]),
|
||||
]
|
||||
for input_text, expected_text, expected_actions in cases:
|
||||
with self.subTest(input_text=input_text):
|
||||
result, actions = self.processor.process_text(input_text)
|
||||
self.assertEqual(result, expected_text)
|
||||
self.assertEqual(actions, expected_actions)
|
||||
|
||||
def test_punctuation_only_segment_is_not_eaten(self) -> None:
|
||||
"""A segment that is only punctuation stays text, not a command."""
|
||||
result, actions = self.processor.process_text("...")
|
||||
self.assertEqual(result, "...")
|
||||
self.assertEqual(actions, [])
|
||||
|
||||
Reference in New Issue
Block a user