whisper-key-local 0.5.3__py3-none-any.whl → 0.6.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. whisper_key/assets/version.txt +1 -1
  2. whisper_key/audio_feedback.py +21 -20
  3. whisper_key/clipboard_manager.py +35 -52
  4. whisper_key/config.defaults.yaml +22 -13
  5. whisper_key/config_manager.py +25 -9
  6. whisper_key/console_manager.py +2 -58
  7. whisper_key/hotkey_listener.py +55 -89
  8. whisper_key/instance_manager.py +9 -13
  9. whisper_key/main.py +28 -11
  10. whisper_key/platform/__init__.py +10 -0
  11. whisper_key/platform/macos/__init__.py +1 -0
  12. whisper_key/platform/macos/app.py +27 -0
  13. whisper_key/platform/macos/assets/tray_idle.png +0 -0
  14. whisper_key/platform/macos/assets/tray_processing.png +0 -0
  15. whisper_key/platform/macos/assets/tray_recording.png +0 -0
  16. whisper_key/platform/macos/console.py +13 -0
  17. whisper_key/platform/macos/hotkeys.py +180 -0
  18. whisper_key/platform/macos/icons.py +11 -0
  19. whisper_key/platform/macos/instance_lock.py +31 -0
  20. whisper_key/platform/macos/keyboard.py +97 -0
  21. whisper_key/platform/macos/keycodes.py +17 -0
  22. whisper_key/platform/macos/paths.py +8 -0
  23. whisper_key/platform/macos/permissions.py +66 -0
  24. whisper_key/platform/windows/__init__.py +1 -0
  25. whisper_key/platform/windows/app.py +6 -0
  26. whisper_key/platform/windows/assets/tray_idle.png +0 -0
  27. whisper_key/platform/windows/assets/tray_processing.png +0 -0
  28. whisper_key/platform/windows/assets/tray_recording.png +0 -0
  29. whisper_key/platform/windows/console.py +59 -0
  30. whisper_key/platform/windows/hotkeys.py +30 -0
  31. whisper_key/platform/windows/icons.py +11 -0
  32. whisper_key/platform/windows/instance_lock.py +14 -0
  33. whisper_key/platform/windows/keyboard.py +12 -0
  34. whisper_key/platform/windows/paths.py +8 -0
  35. whisper_key/platform/windows/permissions.py +6 -0
  36. whisper_key/state_manager.py +4 -4
  37. whisper_key/system_tray.py +30 -57
  38. whisper_key/terminal_ui.py +71 -0
  39. whisper_key/utils.py +8 -4
  40. whisper_key_local-0.6.1.dist-info/METADATA +159 -0
  41. whisper_key_local-0.6.1.dist-info/RECORD +53 -0
  42. {whisper_key_local-0.5.3.dist-info → whisper_key_local-0.6.1.dist-info}/WHEEL +1 -1
  43. whisper_key/assets/tray_idle.png +0 -0
  44. whisper_key/assets/tray_processing.png +0 -0
  45. whisper_key/assets/tray_recording.png +0 -0
  46. whisper_key_local-0.5.3.dist-info/METADATA +0 -130
  47. whisper_key_local-0.5.3.dist-info/RECORD +0 -29
  48. {whisper_key_local-0.5.3.dist-info → whisper_key_local-0.6.1.dist-info}/entry_points.txt +0 -0
  49. {whisper_key_local-0.5.3.dist-info → whisper_key_local-0.6.1.dist-info}/top_level.txt +0 -0
@@ -1 +1 @@
1
- 0.5.3
1
+ 0.6.1
@@ -1,56 +1,57 @@
1
1
  import logging
2
2
  import os
3
+ import platform
3
4
  import threading
4
- import winsound
5
+
6
+ from playsound3 import playsound
7
+
8
+ SOUND_BACKEND = "winmm" if platform.system() == "Windows" else None
5
9
 
6
10
  from .utils import resolve_asset_path
7
11
 
8
- class AudioFeedback:
12
+ class AudioFeedback:
9
13
  def __init__(self, enabled=True, start_sound='', stop_sound='', cancel_sound=''):
10
14
  self.enabled = enabled
11
15
  self.logger = logging.getLogger(__name__)
12
-
16
+
13
17
  self.start_sound_path = resolve_asset_path(start_sound)
14
18
  self.stop_sound_path = resolve_asset_path(stop_sound)
15
19
  self.cancel_sound_path = resolve_asset_path(cancel_sound)
16
-
20
+
17
21
  if not self.enabled:
18
22
  self.logger.info("Audio feedback disabled by configuration")
19
23
  print(" ✗ Audio feedback disabled")
20
24
  else:
21
25
  self._validate_sound_files()
22
26
  print(" ✓ Audio feedback enabled...")
23
-
27
+
24
28
  def _validate_sound_files(self):
25
29
  if self.start_sound_path and not os.path.isfile(self.start_sound_path):
26
30
  self.logger.warning(f"Start sound file not found: {self.start_sound_path}")
27
-
31
+
28
32
  if self.stop_sound_path and not os.path.isfile(self.stop_sound_path):
29
33
  self.logger.warning(f"Stop sound file not found: {self.stop_sound_path}")
30
-
34
+
31
35
  if self.cancel_sound_path and not os.path.isfile(self.cancel_sound_path):
32
36
  self.logger.warning(f"Cancel sound file not found: {self.cancel_sound_path}")
33
-
37
+
34
38
  def _play_sound_file_async(self, file_path: str):
35
- def play_sound():
39
+ def play():
36
40
  try:
37
- # SND_FILENAME = play from file, SND_ASYNC = don't block
38
- winsound.PlaySound(file_path, winsound.SND_FILENAME | winsound.SND_ASYNC)
39
-
41
+ playsound(file_path, block=False, backend=SOUND_BACKEND)
40
42
  except Exception as e:
41
43
  self.logger.warning(f"Failed to play sound file {file_path}: {e}")
42
-
43
- sound_thread = threading.Thread(target=play_sound, daemon=True)
44
- sound_thread.start()
45
-
44
+
45
+ threading.Thread(target=play, daemon=True).start()
46
+
46
47
  def play_start_sound(self):
47
48
  if self.enabled:
48
49
  self._play_sound_file_async(self.start_sound_path)
49
-
50
+
50
51
  def play_stop_sound(self):
51
- if self.enabled:
52
+ if self.enabled:
52
53
  self._play_sound_file_async(self.stop_sound_path)
53
-
54
+
54
55
  def play_cancel_sound(self):
55
56
  if self.enabled:
56
- self._play_sound_file_async(self.cancel_sound_path)
57
+ self._play_sound_file_async(self.cancel_sound_path)
@@ -3,13 +3,10 @@ import time
3
3
  from typing import Optional
4
4
 
5
5
  import pyperclip
6
- import win32gui
7
- import pyautogui
8
6
 
7
+ from .platform import keyboard
9
8
  from .utils import parse_hotkey
10
9
 
11
- pyautogui.FAILSAFE = True # Enable "move mouse to corner to abort automation"
12
-
13
10
  class ClipboardManager:
14
11
  def __init__(self, key_simulation_delay, auto_paste, preserve_clipboard, paste_hotkey):
15
12
  self.logger = logging.getLogger(__name__)
@@ -18,90 +15,77 @@ class ClipboardManager:
18
15
  self.preserve_clipboard = preserve_clipboard
19
16
  self.paste_hotkey = paste_hotkey
20
17
  self.paste_keys = parse_hotkey(paste_hotkey)
21
- self._configure_pyautogui_timing()
18
+ self._configure_keyboard_timing()
22
19
  self._test_clipboard_access()
23
20
  self._print_status()
24
-
25
- def _configure_pyautogui_timing(self):
26
- pyautogui.PAUSE = self.key_simulation_delay
27
-
21
+
22
+ def _configure_keyboard_timing(self):
23
+ keyboard.set_delay(self.key_simulation_delay)
24
+
28
25
  def _test_clipboard_access(self):
29
26
  try:
30
27
  pyperclip.paste()
31
28
  self.logger.info("Clipboard access test successful")
32
-
29
+
33
30
  except Exception as e:
34
31
  self.logger.error(f"Clipboard access test failed: {e}")
35
32
  raise
36
-
33
+
37
34
  def _print_status(self):
38
35
  hotkey_display = self.paste_hotkey.upper()
39
36
  if self.auto_paste:
40
37
  print(f" ✓ Auto-paste is ENABLED using key simulation ({hotkey_display})")
41
38
  else:
42
39
  print(f" ✗ Auto-paste is DISABLED - paste manually with {hotkey_display}")
43
-
40
+
44
41
  def copy_text(self, text: str) -> bool:
45
42
  if not text:
46
43
  return False
47
-
44
+
48
45
  try:
49
46
  self.logger.info(f"Copying text to clipboard ({len(text)} chars)")
50
47
  pyperclip.copy(text)
51
48
  return True
52
-
49
+
53
50
  except Exception as e:
54
51
  self.logger.error(f"Failed to copy text to clipboard: {e}")
55
52
  return False
56
-
53
+
57
54
  def get_clipboard_content(self) -> Optional[str]:
58
55
  try:
59
56
  clipboard_content = pyperclip.paste()
60
-
57
+
61
58
  if clipboard_content:
62
59
  return clipboard_content
63
60
  else:
64
61
  return None
65
-
62
+
66
63
  except Exception as e:
67
64
  self.logger.error(f"Failed to paste text from clipboard: {e}")
68
65
  return None
69
-
66
+
70
67
  def copy_with_notification(self, text: str) -> bool:
71
68
  if not text:
72
69
  return False
73
-
70
+
74
71
  success = self.copy_text(text)
75
-
72
+
76
73
  if success:
77
74
  print(" ✓ Copied to clipboard")
78
75
  print(" ✓ You can now paste with Ctrl+V in any application!")
79
-
76
+
80
77
  return success
81
-
78
+
82
79
  def clear_clipboard(self) -> bool:
83
80
  try:
84
81
  pyperclip.copy("")
85
82
  return True
86
-
83
+
87
84
  except Exception as e:
88
85
  self.logger.error(f"Failed to clear clipboard: {e}")
89
86
  return False
90
-
91
- def get_active_window_handle(self) -> Optional[int]:
92
- try:
93
- hwnd = win32gui.GetForegroundWindow()
94
- if hwnd:
95
- window_title = win32gui.GetWindowText(hwnd)
96
- self.logger.info(f"Active window: '{window_title}' (handle: {hwnd})")
97
- return hwnd
98
- else:
99
- return None
100
- except Exception as e:
101
- self.logger.error(f"Failed to get active window handle: {e}")
102
- return None
103
-
104
- def execute_auto_paste(self, text: str, preserve_clipboard: bool) -> bool:
87
+
88
+ def execute_auto_paste(self, text: str, preserve_clipboard: bool) -> bool:
105
89
  try:
106
90
  original_content = None
107
91
  if preserve_clipboard:
@@ -109,8 +93,8 @@ class ClipboardManager:
109
93
 
110
94
  if not self.copy_text(text):
111
95
  return False
112
-
113
- pyautogui.hotkey(*self.paste_keys)
96
+
97
+ keyboard.send_hotkey(*self.paste_keys)
114
98
 
115
99
  print(f" ✓ Auto-pasted via key simulation")
116
100
 
@@ -119,19 +103,19 @@ class ClipboardManager:
119
103
  time.sleep(self.key_simulation_delay)
120
104
 
121
105
  return True
122
-
106
+
123
107
  except Exception as e:
124
108
  self.logger.error(f"Failed to simulate paste keypress: {e}")
125
109
  return False
126
-
110
+
127
111
  def send_enter_key(self) -> bool:
128
112
  try:
129
113
  self.logger.info("Sending ENTER key to active application")
130
- pyautogui.press('enter')
114
+ keyboard.send_key('enter')
131
115
  print(" ✓ Text submitted with ENTER!")
132
116
 
133
117
  return True
134
-
118
+
135
119
  except Exception as e:
136
120
  self.logger.error(f"Failed to send ENTER key: {e}")
137
121
  return False
@@ -139,30 +123,29 @@ class ClipboardManager:
139
123
  def deliver_transcription(self,
140
124
  transcribed_text: str,
141
125
  use_auto_enter: bool = False) -> bool:
142
-
126
+
143
127
  try:
144
128
  if use_auto_enter:
145
129
  print("🚀 Auto-pasting text and SENDING with ENTER...")
146
-
147
- # Force auto-paste when using auto-enter hotkey
130
+
148
131
  success = self.execute_auto_paste(transcribed_text, self.preserve_clipboard)
149
132
  if success:
150
133
  success = self.send_enter_key()
151
134
 
152
135
  elif self.auto_paste:
153
136
  print("🚀 Auto-pasting text...")
154
- success = self.execute_auto_paste(transcribed_text, self.preserve_clipboard)
155
-
137
+ success = self.execute_auto_paste(transcribed_text, self.preserve_clipboard)
138
+
156
139
  else:
157
140
  print("📋 Copying to clipboard...")
158
- success = self.copy_with_notification(transcribed_text)
141
+ success = self.copy_with_notification(transcribed_text)
159
142
 
160
143
  return success
161
144
 
162
145
  except Exception as e:
163
146
  self.logger.error(f"Delivery workflow failed: {e}")
164
147
  return False
165
-
148
+
166
149
  def update_auto_paste(self, enabled: bool):
167
150
  self.auto_paste = enabled
168
- self._print_status()
151
+ self._print_status()
@@ -3,9 +3,10 @@
3
3
  # =============================================================================
4
4
  # DO NOT EDIT THIS FILE
5
5
  # This is the default configuration template for Whisper Key
6
- #
6
+ #
7
7
  # Personal settings are stored at:
8
- # %APPDATA%\Roaming\whisperkey\user_settings.yaml
8
+ # Windows: %APPDATA%\whisperkey\user_settings.yaml
9
+ # macOS: ~/Library/Application Support/whisperkey/user_settings.yaml
9
10
 
10
11
  whisper: # Whisper AI Model Settings
11
12
 
@@ -15,7 +16,12 @@ whisper: # Whisper AI Model Settings
15
16
 
16
17
  # Processing device - where the AI runs
17
18
  # Options: "cpu", "cuda" (for NVIDIA GPUs)
18
- # Note: "cuda" requires compatible GPU and drivers
19
+ #
20
+ # CUDA Setup (for NVIDIA GPUs only):
21
+ # 1. Install CUDA 12:
22
+ # winget install Nvidia.CUDA --version 12.9
23
+ # Or download from: https://developer.nvidia.com/cuda-downloads
24
+ # 2. Set device: cuda and compute_type: float16 below
19
25
  device: cpu
20
26
 
21
27
  # Compute precision - affects speed vs accuracy
@@ -89,14 +95,16 @@ hotkey: # Hotkey Configuration
89
95
 
90
96
  # Key combination to start/stop recording
91
97
  # Format: modifier+modifier+key (use lowercase)
92
- # Common modifiers: ctrl, shift, alt, win
98
+ # Windows modifiers: ctrl, shift, alt, win
99
+ # macOS modifiers: cmd, ctrl, shift, option, fn
93
100
  # Examples:
94
- # - "ctrl+shift+space" (default)
101
+ # - "ctrl+win" (Windows default)
102
+ # - "fn+ctrl" (macOS default)
95
103
  # - "ctrl+alt+r"
96
- # - "win+shift+v"
104
+ # - "win+v"
97
105
  # - "ctrl+shift+f12"
98
- recording_hotkey: ctrl+win
99
-
106
+ recording_hotkey: "ctrl+win | macos: fn+ctrl"
107
+
100
108
  # Stop recording with just the first modifier key
101
109
  # If enabled, stops recording with first modifier of the main hotkey
102
110
  # (e.g., if combination is 'ctrl+win', pressing 'ctrl' will stop)
@@ -109,12 +117,13 @@ hotkey: # Hotkey Configuration
109
117
  auto_enter_enabled: true
110
118
 
111
119
  # Key combination for auto-ENTER function
112
- auto_enter_combination: alt
120
+ auto_enter_combination: "alt | macos: option"
113
121
 
114
122
  # Key combination to cancel recording
115
123
  # Format: modifier+modifier+key (use lowercase) or single key
116
- # Examples: "esc", "ctrl+c", "shift+esc"
117
- cancel_combination: esc
124
+ # Examples: "esc", "ctrl+c", "shift+esc"
125
+ # Note: On macOS, ESC is system-reserved
126
+ cancel_combination: "esc | macos: shift"
118
127
 
119
128
  vad: # Voice Activity Detection (VAD)
120
129
 
@@ -169,7 +178,7 @@ clipboard: # Clipboard Behavior
169
178
 
170
179
  # Automatically paste after transcription
171
180
  # true = paste immediately to active window
172
- # false = only copy to clipboard (paste manually with Ctrl+V)
181
+ # false = only copy to clipboard (paste manually)
173
182
  auto_paste: true
174
183
 
175
184
  # Key combination to simulate paste
@@ -178,7 +187,7 @@ clipboard: # Clipboard Behavior
178
187
  # - "ctrl+v" (standard paste)
179
188
  # - "ctrl+shift+v" (plain text paste in some apps)
180
189
  # - "shift+insert" (terminal paste)
181
- paste_hotkey: ctrl+v
190
+ paste_hotkey: "ctrl+v | macos: cmd+v"
182
191
 
183
192
  # Preserve existing clipboard content when pasting
184
193
  # true = save current clipboard, paste transcription, then restore original clipboard
@@ -1,28 +1,45 @@
1
1
  import os
2
2
  import logging
3
- import shutil
4
3
  import platform
4
+ import shutil
5
5
  from typing import Dict, Any, Optional
6
6
  from io import StringIO
7
7
 
8
8
  from ruamel.yaml import YAML
9
9
 
10
10
  from .utils import resolve_asset_path, beautify_hotkey, get_user_app_data_path
11
+ from .platform import IS_MACOS
11
12
 
12
13
  def deep_merge_config(default_config: Dict[str, Any],
13
14
  user_config: Dict[str, Any]) -> Dict[str, Any]:
14
-
15
+
15
16
  result = default_config.copy()
16
-
17
+
17
18
  for key, value in user_config.items():
18
19
  if key in result and isinstance(result[key], dict) and isinstance(value, dict):
19
20
  result[key] = deep_merge_config(result[key], value)
20
21
  else:
21
22
  result[key] = value
22
-
23
+
23
24
  return result
24
25
 
25
26
 
27
+ def _parse_platform_value(value: str) -> str:
28
+ parts = value.split(' | macos:')
29
+ default_value = parts[0].strip()
30
+ macos_value = parts[1].strip() if len(parts) > 1 else default_value
31
+ return macos_value if IS_MACOS else default_value
32
+
33
+
34
+ def _resolve_platform_values(config: Dict[str, Any]) -> Dict[str, Any]:
35
+ for key, value in config.items():
36
+ if isinstance(value, dict):
37
+ _resolve_platform_values(value)
38
+ elif isinstance(value, str) and ' | macos:' in value:
39
+ config[key] = _parse_platform_value(value)
40
+ return config
41
+
42
+
26
43
  class ConfigManager:
27
44
  def __init__(self, config_path: str = None, use_user_settings: bool = True):
28
45
  if config_path is None:
@@ -109,9 +126,10 @@ class ConfigManager:
109
126
 
110
127
  self._remove_unused_keys_from_user_config(user_config, default_config)
111
128
  merged_config = deep_merge_config(default_config, user_config)
129
+ resolved_config = _resolve_platform_values(merged_config)
112
130
  self.logger.info(f"Loaded user configuration from {self.config_path}")
113
-
114
- validated_config = self.validator.fix_config(merged_config, default_config)
131
+
132
+ validated_config = self.validator.fix_config(resolved_config, default_config)
115
133
  self.config = validated_config
116
134
 
117
135
  self.save_config_to_user_settings_file()
@@ -125,7 +143,7 @@ class ConfigManager:
125
143
  self.logger.error(f"Error loading user config file: {e}")
126
144
 
127
145
  self.logger.info(f"Using default configuration from {self.default_config_path}")
128
- return default_config
146
+ return _resolve_platform_values(default_config)
129
147
 
130
148
  def _load_default_config(self) -> Dict[str, Any]:
131
149
  try:
@@ -272,8 +290,6 @@ class ConfigManager:
272
290
  self.config[section][key] = value
273
291
  self.save_config_to_user_settings_file()
274
292
 
275
- print(f"⚙️ Updated {section} setting")
276
-
277
293
  self.logger.debug(f"Updated setting {section}.{key}: {old_value} -> {value}")
278
294
  else:
279
295
  self.logger.error(f"Setting {section}:{key} does not exist")
@@ -1,59 +1,3 @@
1
- import win32console
2
- import win32gui
3
- import win32con
4
- import logging
5
- import threading
6
- import time
1
+ from .platform import console
7
2
 
8
- class ConsoleManager:
9
- def __init__(self, config: dict, is_executable_mode: bool = False):
10
- self.logger = logging.getLogger(__name__)
11
- self.config = config
12
- self.is_executable_mode = is_executable_mode
13
- self.console_handle = None
14
- self._lock = threading.Lock()
15
-
16
- try:
17
- self.console_handle = self.get_console_window()
18
- except Exception as e:
19
- self.logger.error(f"Failed to initialize console manager: {e}")
20
-
21
- self.hide_on_startup()
22
-
23
- def get_console_window(self):
24
- try:
25
- handle = win32console.GetConsoleWindow()
26
- if handle:
27
- self.logger.debug(f"Console window handle: {handle}")
28
- return handle
29
- else:
30
- self.logger.warning("GetConsoleWindow returned null handle")
31
- return None
32
- except Exception as e:
33
- self.logger.error(f"Failed to get console window: {e}")
34
- return None
35
-
36
- def hide_on_startup(self):
37
- if self.config.get('start_hidden', False) and self.is_executable_mode:
38
- time.sleep(2)
39
- self.hide_console()
40
-
41
- def show_console(self):
42
- with self._lock:
43
- try:
44
- win32gui.ShowWindow(self.console_handle, win32con.SW_HIDE)
45
- win32gui.ShowWindow(self.console_handle, win32con.SW_RESTORE)
46
- win32gui.SetForegroundWindow(self.console_handle)
47
- return True
48
- except Exception as e:
49
- self.logger.error(f"Failed to show/focus console: {e}")
50
- return False
51
-
52
- def hide_console(self):
53
- with self._lock:
54
- try:
55
- win32gui.ShowWindow(self.console_handle, win32con.SW_HIDE)
56
- return True
57
- except Exception as e:
58
- self.logger.error(f"Failed to hide console: {e}")
59
- return False
3
+ ConsoleManager = console.ConsoleManager