diff --git a/pyproject.toml b/pyproject.toml index 737f73b..86bdb99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ dependencies = [ "numpy>=2.0.0", "evdev>=1.7.0; sys_platform == 'linux'", "pynput>=1.7.0; sys_platform == 'win32'", + "pywin32>=306; sys_platform == 'win32'", ] [project.scripts] diff --git a/tests/test_inserter.py b/tests/test_inserter.py index 5047e57..475cfb3 100644 --- a/tests/test_inserter.py +++ b/tests/test_inserter.py @@ -64,10 +64,10 @@ class TestWaylandInserter: class TestWin32Inserter: @pytest.mark.asyncio - @patch("whisper_local.inserter._win32.keyboard_controller") + @patch("whisper_local.inserter._win32.Win32Inserter._send_paste") @patch("whisper_local.inserter._win32.Win32Inserter._get_clipboard", return_value="alter clipboard") @patch("whisper_local.inserter._win32.Win32Inserter._set_clipboard") - async def test_insert_text(self, mock_set, mock_get, mock_kb): + async def test_insert_text(self, mock_set, mock_get, mock_paste): from whisper_local.inserter._win32 import Win32Inserter inserter = Win32Inserter() @@ -75,7 +75,9 @@ class TestWin32Inserter: # Clipboard wurde gesichert mock_get.assert_called_once() - # Text wurde in Clipboard gesetzt, dann Ctrl+V, dann Restore + # Ctrl+V wurde simuliert + mock_paste.assert_called_once() + # Text wurde in Clipboard gesetzt, dann Restore assert mock_set.call_count == 2 mock_set.assert_any_call("Hallo Welt") mock_set.assert_any_call("alter clipboard") @@ -88,14 +90,12 @@ class TestWin32Inserter: await inserter.insert("") @pytest.mark.asyncio - @patch("whisper_local.inserter._win32.keyboard_controller") + @patch("whisper_local.inserter._win32.Win32Inserter._send_paste", side_effect=OSError("keyboard error")) @patch("whisper_local.inserter._win32.Win32Inserter._get_clipboard", return_value="original") @patch("whisper_local.inserter._win32.Win32Inserter._set_clipboard") - async def test_clipboard_restored_on_error(self, mock_set, mock_get, mock_kb): + async def test_clipboard_restored_on_error(self, mock_set, mock_get, mock_paste): from whisper_local.inserter._win32 import Win32Inserter - mock_kb.press.side_effect = OSError("keyboard error") - inserter = Win32Inserter() with pytest.raises(OSError): await inserter.insert("Test") diff --git a/uv.lock b/uv.lock index 562c45a..70fbde4 100644 --- a/uv.lock +++ b/uv.lock @@ -517,6 +517,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/b8/ff33610932e0ee81ae7f1269c890f697d56ff74b9f5b2ee5d9b7fa2c5355/python_xlib-0.33-py2.py3-none-any.whl", hash = "sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398", size = 182185, upload-time = "2022-12-25T18:52:58.662Z" }, ] +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -692,6 +705,7 @@ dependencies = [ { name = "faster-whisper" }, { name = "numpy" }, { name = "pynput", marker = "sys_platform == 'win32'" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "sounddevice" }, ] @@ -707,6 +721,7 @@ requires-dist = [ { name = "faster-whisper", specifier = ">=1.1.0" }, { name = "numpy", specifier = ">=2.0.0" }, { name = "pynput", marker = "sys_platform == 'win32'", specifier = ">=1.7.0" }, + { name = "pywin32", marker = "sys_platform == 'win32'", specifier = ">=306" }, { name = "sounddevice", specifier = ">=0.5.0" }, ] diff --git a/whisper_local/inserter/_win32.py b/whisper_local/inserter/_win32.py index 8ddb7a8..fcb473f 100644 --- a/whisper_local/inserter/_win32.py +++ b/whisper_local/inserter/_win32.py @@ -7,12 +7,13 @@ from pynput.keyboard import Controller, Key logger = logging.getLogger(__name__) -keyboard_controller = Controller() - PASTE_DELAY = 0.2 class Win32Inserter: + def __init__(self): + self._keyboard = Controller() + def _get_clipboard(self) -> str: """Liest den aktuellen Clipboard-Inhalt (Text).""" import win32clipboard @@ -38,6 +39,13 @@ class Win32Inserter: finally: win32clipboard.CloseClipboard() + def _send_paste(self) -> None: + """Simuliert Ctrl+V.""" + self._keyboard.press(Key.ctrl) + self._keyboard.press('v') + self._keyboard.release('v') + self._keyboard.release(Key.ctrl) + async def insert(self, text: str) -> None: """Fügt Text ins aktive Fenster ein via Clipboard + Ctrl+V.""" if not text: @@ -48,12 +56,7 @@ class Win32Inserter: try: await asyncio.to_thread(self._set_clipboard, text) await asyncio.sleep(0.05) - - keyboard_controller.press(Key.ctrl) - keyboard_controller.press('v') - keyboard_controller.release('v') - keyboard_controller.release(Key.ctrl) - + await asyncio.to_thread(self._send_paste) await asyncio.sleep(PASTE_DELAY) finally: await asyncio.to_thread(self._set_clipboard, old_clipboard)