fix: add pywin32 dep, move Controller into class, wrap keyboard in to_thread

This commit is contained in:
Vitali Graf
2026-04-08 10:43:14 +02:00
parent 670ffabb1f
commit b94c58d628
4 changed files with 34 additions and 15 deletions
+1
View File
@@ -8,6 +8,7 @@ dependencies = [
"numpy>=2.0.0", "numpy>=2.0.0",
"evdev>=1.7.0; sys_platform == 'linux'", "evdev>=1.7.0; sys_platform == 'linux'",
"pynput>=1.7.0; sys_platform == 'win32'", "pynput>=1.7.0; sys_platform == 'win32'",
"pywin32>=306; sys_platform == 'win32'",
] ]
[project.scripts] [project.scripts]
+7 -7
View File
@@ -64,10 +64,10 @@ class TestWaylandInserter:
class TestWin32Inserter: class TestWin32Inserter:
@pytest.mark.asyncio @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._get_clipboard", return_value="alter clipboard")
@patch("whisper_local.inserter._win32.Win32Inserter._set_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 from whisper_local.inserter._win32 import Win32Inserter
inserter = Win32Inserter() inserter = Win32Inserter()
@@ -75,7 +75,9 @@ class TestWin32Inserter:
# Clipboard wurde gesichert # Clipboard wurde gesichert
mock_get.assert_called_once() 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 assert mock_set.call_count == 2
mock_set.assert_any_call("Hallo Welt") mock_set.assert_any_call("Hallo Welt")
mock_set.assert_any_call("alter clipboard") mock_set.assert_any_call("alter clipboard")
@@ -88,14 +90,12 @@ class TestWin32Inserter:
await inserter.insert("") await inserter.insert("")
@pytest.mark.asyncio @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._get_clipboard", return_value="original")
@patch("whisper_local.inserter._win32.Win32Inserter._set_clipboard") @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 from whisper_local.inserter._win32 import Win32Inserter
mock_kb.press.side_effect = OSError("keyboard error")
inserter = Win32Inserter() inserter = Win32Inserter()
with pytest.raises(OSError): with pytest.raises(OSError):
await inserter.insert("Test") await inserter.insert("Test")
Generated
+15
View File
@@ -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" }, { 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]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0.3" version = "6.0.3"
@@ -692,6 +705,7 @@ dependencies = [
{ name = "faster-whisper" }, { name = "faster-whisper" },
{ name = "numpy" }, { name = "numpy" },
{ name = "pynput", marker = "sys_platform == 'win32'" }, { name = "pynput", marker = "sys_platform == 'win32'" },
{ name = "pywin32", marker = "sys_platform == 'win32'" },
{ name = "sounddevice" }, { name = "sounddevice" },
] ]
@@ -707,6 +721,7 @@ requires-dist = [
{ name = "faster-whisper", specifier = ">=1.1.0" }, { name = "faster-whisper", specifier = ">=1.1.0" },
{ name = "numpy", specifier = ">=2.0.0" }, { name = "numpy", specifier = ">=2.0.0" },
{ name = "pynput", marker = "sys_platform == 'win32'", specifier = ">=1.7.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" }, { name = "sounddevice", specifier = ">=0.5.0" },
] ]
+11 -8
View File
@@ -7,12 +7,13 @@ from pynput.keyboard import Controller, Key
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
keyboard_controller = Controller()
PASTE_DELAY = 0.2 PASTE_DELAY = 0.2
class Win32Inserter: class Win32Inserter:
def __init__(self):
self._keyboard = Controller()
def _get_clipboard(self) -> str: def _get_clipboard(self) -> str:
"""Liest den aktuellen Clipboard-Inhalt (Text).""" """Liest den aktuellen Clipboard-Inhalt (Text)."""
import win32clipboard import win32clipboard
@@ -38,6 +39,13 @@ class Win32Inserter:
finally: finally:
win32clipboard.CloseClipboard() 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: async def insert(self, text: str) -> None:
"""Fügt Text ins aktive Fenster ein via Clipboard + Ctrl+V.""" """Fügt Text ins aktive Fenster ein via Clipboard + Ctrl+V."""
if not text: if not text:
@@ -48,12 +56,7 @@ class Win32Inserter:
try: try:
await asyncio.to_thread(self._set_clipboard, text) await asyncio.to_thread(self._set_clipboard, text)
await asyncio.sleep(0.05) await asyncio.sleep(0.05)
await asyncio.to_thread(self._send_paste)
keyboard_controller.press(Key.ctrl)
keyboard_controller.press('v')
keyboard_controller.release('v')
keyboard_controller.release(Key.ctrl)
await asyncio.sleep(PASTE_DELAY) await asyncio.sleep(PASTE_DELAY)
finally: finally:
await asyncio.to_thread(self._set_clipboard, old_clipboard) await asyncio.to_thread(self._set_clipboard, old_clipboard)