whisper-key-local 0.5.3__tar.gz → 0.6.1__tar.gz

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 (66) hide show
  1. whisper_key_local-0.6.1/PKG-INFO +159 -0
  2. whisper_key_local-0.6.1/README.md +134 -0
  3. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/pyproject.toml +14 -6
  4. whisper_key_local-0.6.1/src/whisper_key/assets/version.txt +1 -0
  5. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key/audio_feedback.py +21 -20
  6. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key/clipboard_manager.py +35 -52
  7. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key/config.defaults.yaml +22 -13
  8. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key/config_manager.py +25 -9
  9. whisper_key_local-0.6.1/src/whisper_key/console_manager.py +3 -0
  10. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key/hotkey_listener.py +55 -89
  11. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key/instance_manager.py +9 -13
  12. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key/main.py +28 -11
  13. whisper_key_local-0.6.1/src/whisper_key/platform/__init__.py +10 -0
  14. whisper_key_local-0.6.1/src/whisper_key/platform/macos/__init__.py +1 -0
  15. whisper_key_local-0.6.1/src/whisper_key/platform/macos/app.py +27 -0
  16. whisper_key_local-0.6.1/src/whisper_key/platform/macos/assets/tray_idle.png +0 -0
  17. whisper_key_local-0.6.1/src/whisper_key/platform/macos/assets/tray_processing.png +0 -0
  18. whisper_key_local-0.6.1/src/whisper_key/platform/macos/assets/tray_recording.png +0 -0
  19. whisper_key_local-0.6.1/src/whisper_key/platform/macos/console.py +13 -0
  20. whisper_key_local-0.6.1/src/whisper_key/platform/macos/hotkeys.py +180 -0
  21. whisper_key_local-0.6.1/src/whisper_key/platform/macos/icons.py +11 -0
  22. whisper_key_local-0.6.1/src/whisper_key/platform/macos/instance_lock.py +31 -0
  23. whisper_key_local-0.6.1/src/whisper_key/platform/macos/keyboard.py +97 -0
  24. whisper_key_local-0.6.1/src/whisper_key/platform/macos/keycodes.py +17 -0
  25. whisper_key_local-0.6.1/src/whisper_key/platform/macos/paths.py +8 -0
  26. whisper_key_local-0.6.1/src/whisper_key/platform/macos/permissions.py +66 -0
  27. whisper_key_local-0.6.1/src/whisper_key/platform/windows/__init__.py +1 -0
  28. whisper_key_local-0.6.1/src/whisper_key/platform/windows/app.py +6 -0
  29. whisper_key_local-0.6.1/src/whisper_key/platform/windows/assets/tray_idle.png +0 -0
  30. whisper_key_local-0.6.1/src/whisper_key/platform/windows/assets/tray_processing.png +0 -0
  31. whisper_key_local-0.6.1/src/whisper_key/platform/windows/assets/tray_recording.png +0 -0
  32. whisper_key_local-0.5.3/src/whisper_key/console_manager.py → whisper_key_local-0.6.1/src/whisper_key/platform/windows/console.py +4 -4
  33. whisper_key_local-0.6.1/src/whisper_key/platform/windows/hotkeys.py +30 -0
  34. whisper_key_local-0.6.1/src/whisper_key/platform/windows/icons.py +11 -0
  35. whisper_key_local-0.6.1/src/whisper_key/platform/windows/instance_lock.py +14 -0
  36. whisper_key_local-0.6.1/src/whisper_key/platform/windows/keyboard.py +12 -0
  37. whisper_key_local-0.6.1/src/whisper_key/platform/windows/paths.py +8 -0
  38. whisper_key_local-0.6.1/src/whisper_key/platform/windows/permissions.py +6 -0
  39. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key/state_manager.py +4 -4
  40. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key/system_tray.py +30 -57
  41. whisper_key_local-0.6.1/src/whisper_key/terminal_ui.py +71 -0
  42. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key/utils.py +8 -4
  43. whisper_key_local-0.6.1/src/whisper_key_local.egg-info/PKG-INFO +159 -0
  44. whisper_key_local-0.6.1/src/whisper_key_local.egg-info/SOURCES.txt +56 -0
  45. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key_local.egg-info/requires.txt +7 -1
  46. whisper_key_local-0.5.3/PKG-INFO +0 -130
  47. whisper_key_local-0.5.3/README.md +0 -109
  48. whisper_key_local-0.5.3/src/whisper_key/assets/tray_idle.png +0 -0
  49. whisper_key_local-0.5.3/src/whisper_key/assets/tray_processing.png +0 -0
  50. whisper_key_local-0.5.3/src/whisper_key/assets/tray_recording.png +0 -0
  51. whisper_key_local-0.5.3/src/whisper_key/assets/version.txt +0 -1
  52. whisper_key_local-0.5.3/src/whisper_key_local.egg-info/PKG-INFO +0 -130
  53. whisper_key_local-0.5.3/src/whisper_key_local.egg-info/SOURCES.txt +0 -32
  54. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/setup.cfg +0 -0
  55. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key/__init__.py +0 -0
  56. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key/assets/portaudio.dll +0 -0
  57. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key/assets/sounds/record_cancel.wav +0 -0
  58. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key/assets/sounds/record_start.wav +0 -0
  59. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key/assets/sounds/record_stop.wav +0 -0
  60. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key/audio_recorder.py +0 -0
  61. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key/model_registry.py +0 -0
  62. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key/voice_activity_detection.py +0 -0
  63. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key/whisper_engine.py +0 -0
  64. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key_local.egg-info/dependency_links.txt +0 -0
  65. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key_local.egg-info/entry_points.txt +0 -0
  66. {whisper_key_local-0.5.3 → whisper_key_local-0.6.1}/src/whisper_key_local.egg-info/top_level.txt +0 -0
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.4
2
+ Name: whisper-key-local
3
+ Version: 0.6.1
4
+ Summary: Local faster-whisper speech-to-text app with global hotkeys for Windows
5
+ Author-email: Pin Wang <pinwang@gmail.com>
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: faster-whisper>=1.2.1
9
+ Requires-Dist: ctranslate2>=4.6.3
10
+ Requires-Dist: numpy>=1.24.0
11
+ Requires-Dist: soxr>=0.3.0
12
+ Requires-Dist: sounddevice>=0.4.6
13
+ Requires-Dist: pyperclip>=1.8.2
14
+ Requires-Dist: ruamel.yaml>=0.18.14
15
+ Requires-Dist: pystray>=0.19.5
16
+ Requires-Dist: Pillow>=10.0.0
17
+ Requires-Dist: hf-xet>=1.1.5
18
+ Requires-Dist: playsound3>=2.0
19
+ Requires-Dist: ten-vad>=1.0.6
20
+ Requires-Dist: global-hotkeys>=0.1.7; sys_platform == "win32"
21
+ Requires-Dist: pywin32>=306; sys_platform == "win32"
22
+ Requires-Dist: pyautogui>=0.9.54; sys_platform == "win32"
23
+ Requires-Dist: pyobjc-framework-Quartz; sys_platform == "darwin"
24
+ Requires-Dist: pyobjc-framework-ApplicationServices; sys_platform == "darwin"
25
+
26
+ # Whisper Key - Local Speech-to-Text
27
+
28
+ Global hotkeys to record speech and transcribe directly to your cursor.
29
+
30
+ > **Now on Windows and macOS!** Questions or ideas? [Discord](https://discord.gg/uZnXV8snhz)
31
+
32
+ ## ✨ Features
33
+
34
+ - **Global Hotkey**: Start recording speech from any app
35
+ - **Auto-Paste**: Transcribe directly to cursor
36
+ - **Auto-Send**: Optionally auto-send your transcription with ENTER
37
+ - **Offline Capable**: No internet required after models downloaded
38
+ - **Local Processing**: Voice data never leaves your computer
39
+ - **Efficient Models**: Choose a small, efficient model for CPU
40
+ - **CUDA Support**: Or leverage your nVidia GPU with bigger models
41
+ - **Voice activity detection**: Auto-cancel after long silences and prevent hallucination
42
+ - **Cross-platform**: Works on Windows and macOS
43
+ - **Configurable**: Customize hotkeys, models, and [much more](#️-configuration)
44
+
45
+ ## 🚀 Quick Start
46
+
47
+ ### From PyPI (Recommended)
48
+
49
+ Requires Python 3.11-3.13
50
+
51
+ ```bash
52
+ # With pipx (isolated environment)
53
+ pipx install whisper-key-local
54
+
55
+ # Or with pip (simpler)
56
+ pip install whisper-key-local
57
+ ```
58
+
59
+ Then run: `whisper-key`
60
+
61
+ ### Portable App (Windows Only)
62
+
63
+ 1. [Download the latest release zip](https://github.com/PinW/whisper-key-local/releases/latest)
64
+ 2. Extract and run `whisper-key.exe`
65
+
66
+ ### From Source
67
+
68
+ ```bash
69
+ git clone https://github.com/PinW/whisper-key-local.git
70
+ cd whisper-key-local
71
+ pip install -e .
72
+ python whisper-key.py
73
+ ```
74
+
75
+ ## 🎤 Basic Usage
76
+
77
+ | Hotkey | Windows | macOS |
78
+ |--------|---------|-------|
79
+ | Start recording | `Ctrl+Win` | `Fn+Ctrl` |
80
+ | Stop & transcribe | `Ctrl` | `Fn` |
81
+ | Stop & auto-send | `Alt` | `Option` |
82
+ | Cancel recording | `Esc` | `Shift` |
83
+
84
+ Open the system tray / menu bar icon to:
85
+ - Toggle auto-paste vs clipboard-only
86
+ - Change transcription model
87
+ - Select audio device
88
+
89
+ ## ⚙️ Configuration
90
+
91
+ Local settings at:
92
+ - **Windows:** `%APPDATA%\whisperkey\user_settings.yaml`
93
+ - **macOS:** `~/Library/Application Support/whisperkey/user_settings.yaml`
94
+
95
+ Delete this file and restart app to reset to defaults.
96
+
97
+ | Option | Default | Notes |
98
+ |--------|---------|-------|
99
+ | **Whisper** |||
100
+ | `whisper.model` | `tiny` | Any model defined in `whisper.models` |
101
+ | `whisper.device` | `cpu` | cpu or cuda (NVIDIA GPU) |
102
+ | `whisper.compute_type` | `int8` | int8/float16/float32 |
103
+ | `whisper.language` | `auto` | auto or language code (en, es, fr, etc.) |
104
+ | `whisper.beam_size` | `5` | Higher = more accurate but slower (1-10) |
105
+ | `whisper.models` | (see config) | Add custom HuggingFace or local models |
106
+ | **Hotkeys** |||
107
+ | `hotkey.recording_hotkey` | `ctrl+win` / `fn+ctrl` | Windows / macOS |
108
+ | `hotkey.stop_with_modifier_enabled` | `true` | Stop with first modifier only |
109
+ | `hotkey.auto_enter_enabled` | `true` | Enable auto-send hotkey |
110
+ | `hotkey.auto_enter_combination` | `alt` / `option` | Stop + paste + Enter |
111
+ | `hotkey.cancel_combination` | `esc` / `shift` | Cancel recording |
112
+ | **Voice Activity Detection** |||
113
+ | `vad.vad_precheck_enabled` | `true` | Prevent hallucinations on silence |
114
+ | `vad.vad_onset_threshold` | `0.7` | Speech detection start (0.0-1.0) |
115
+ | `vad.vad_offset_threshold` | `0.55` | Speech detection end (0.0-1.0) |
116
+ | `vad.vad_min_speech_duration` | `0.1` | Min speech segment (seconds) |
117
+ | `vad.vad_realtime_enabled` | `true` | Auto-stop on silence |
118
+ | `vad.vad_silence_timeout_seconds` | `30.0` | Seconds before auto-stop |
119
+ | **Audio** |||
120
+ | `audio.host` | `null` | Audio API (WASAPI, Core Audio, etc.) |
121
+ | `audio.channels` | `1` | 1 = mono, 2 = stereo |
122
+ | `audio.dtype` | `float32` | float32/int16/int24/int32 |
123
+ | `audio.max_duration` | `900` | Max recording seconds (0 = unlimited) |
124
+ | `audio.input_device` | `default` | Device ID or "default" |
125
+ | **Clipboard** |||
126
+ | `clipboard.auto_paste` | `true` | false = clipboard only |
127
+ | `clipboard.paste_hotkey` | `ctrl+v` / `cmd+v` | Paste key simulation |
128
+ | `clipboard.preserve_clipboard` | `true` | Restore clipboard after paste |
129
+ | `clipboard.key_simulation_delay` | `0.05` | Delay between keystrokes (seconds) |
130
+ | **Logging** |||
131
+ | `logging.level` | `INFO` | DEBUG/INFO/WARNING/ERROR/CRITICAL |
132
+ | `logging.file.enabled` | `true` | Write to app.log |
133
+ | `logging.console.enabled` | `true` | Print to console |
134
+ | `logging.console.level` | `WARNING` | Console verbosity |
135
+ | **Audio Feedback** |||
136
+ | `audio_feedback.enabled` | `true` | Play sounds on record/stop |
137
+ | `audio_feedback.start_sound` | `assets/sounds/...` | Custom sound file path |
138
+ | `audio_feedback.stop_sound` | `assets/sounds/...` | Custom sound file path |
139
+ | `audio_feedback.cancel_sound` | `assets/sounds/...` | Custom sound file path |
140
+ | **System Tray** |||
141
+ | `system_tray.enabled` | `true` | Show tray icon |
142
+ | `system_tray.tooltip` | `Whisper Key` | Hover text |
143
+ | **Console** |||
144
+ | `console.start_hidden` | `false` | Start minimized to tray |
145
+
146
+ ## 📁 Model Cache
147
+
148
+ Default path for transcription models (via HuggingFace):
149
+ - **Windows:** `%USERPROFILE%\.cache\huggingface\hub\`
150
+ - **macOS:** `~/.cache/huggingface/hub/`
151
+
152
+ ## 📦 Dependencies
153
+
154
+ **Cross-platform:**
155
+ `faster-whisper` · `numpy` · `sounddevice` · `soxr` · `pyperclip` · `ruamel.yaml` · `pystray` · `Pillow` · `playsound3` · `ten-vad` · `hf-xet`
156
+
157
+ **Windows:** `global-hotkeys` · `pywin32` · `pyautogui`
158
+
159
+ **macOS:** `pyobjc-framework-Quartz` · `pyobjc-framework-ApplicationServices`
@@ -0,0 +1,134 @@
1
+ # Whisper Key - Local Speech-to-Text
2
+
3
+ Global hotkeys to record speech and transcribe directly to your cursor.
4
+
5
+ > **Now on Windows and macOS!** Questions or ideas? [Discord](https://discord.gg/uZnXV8snhz)
6
+
7
+ ## ✨ Features
8
+
9
+ - **Global Hotkey**: Start recording speech from any app
10
+ - **Auto-Paste**: Transcribe directly to cursor
11
+ - **Auto-Send**: Optionally auto-send your transcription with ENTER
12
+ - **Offline Capable**: No internet required after models downloaded
13
+ - **Local Processing**: Voice data never leaves your computer
14
+ - **Efficient Models**: Choose a small, efficient model for CPU
15
+ - **CUDA Support**: Or leverage your nVidia GPU with bigger models
16
+ - **Voice activity detection**: Auto-cancel after long silences and prevent hallucination
17
+ - **Cross-platform**: Works on Windows and macOS
18
+ - **Configurable**: Customize hotkeys, models, and [much more](#️-configuration)
19
+
20
+ ## 🚀 Quick Start
21
+
22
+ ### From PyPI (Recommended)
23
+
24
+ Requires Python 3.11-3.13
25
+
26
+ ```bash
27
+ # With pipx (isolated environment)
28
+ pipx install whisper-key-local
29
+
30
+ # Or with pip (simpler)
31
+ pip install whisper-key-local
32
+ ```
33
+
34
+ Then run: `whisper-key`
35
+
36
+ ### Portable App (Windows Only)
37
+
38
+ 1. [Download the latest release zip](https://github.com/PinW/whisper-key-local/releases/latest)
39
+ 2. Extract and run `whisper-key.exe`
40
+
41
+ ### From Source
42
+
43
+ ```bash
44
+ git clone https://github.com/PinW/whisper-key-local.git
45
+ cd whisper-key-local
46
+ pip install -e .
47
+ python whisper-key.py
48
+ ```
49
+
50
+ ## 🎤 Basic Usage
51
+
52
+ | Hotkey | Windows | macOS |
53
+ |--------|---------|-------|
54
+ | Start recording | `Ctrl+Win` | `Fn+Ctrl` |
55
+ | Stop & transcribe | `Ctrl` | `Fn` |
56
+ | Stop & auto-send | `Alt` | `Option` |
57
+ | Cancel recording | `Esc` | `Shift` |
58
+
59
+ Open the system tray / menu bar icon to:
60
+ - Toggle auto-paste vs clipboard-only
61
+ - Change transcription model
62
+ - Select audio device
63
+
64
+ ## ⚙️ Configuration
65
+
66
+ Local settings at:
67
+ - **Windows:** `%APPDATA%\whisperkey\user_settings.yaml`
68
+ - **macOS:** `~/Library/Application Support/whisperkey/user_settings.yaml`
69
+
70
+ Delete this file and restart app to reset to defaults.
71
+
72
+ | Option | Default | Notes |
73
+ |--------|---------|-------|
74
+ | **Whisper** |||
75
+ | `whisper.model` | `tiny` | Any model defined in `whisper.models` |
76
+ | `whisper.device` | `cpu` | cpu or cuda (NVIDIA GPU) |
77
+ | `whisper.compute_type` | `int8` | int8/float16/float32 |
78
+ | `whisper.language` | `auto` | auto or language code (en, es, fr, etc.) |
79
+ | `whisper.beam_size` | `5` | Higher = more accurate but slower (1-10) |
80
+ | `whisper.models` | (see config) | Add custom HuggingFace or local models |
81
+ | **Hotkeys** |||
82
+ | `hotkey.recording_hotkey` | `ctrl+win` / `fn+ctrl` | Windows / macOS |
83
+ | `hotkey.stop_with_modifier_enabled` | `true` | Stop with first modifier only |
84
+ | `hotkey.auto_enter_enabled` | `true` | Enable auto-send hotkey |
85
+ | `hotkey.auto_enter_combination` | `alt` / `option` | Stop + paste + Enter |
86
+ | `hotkey.cancel_combination` | `esc` / `shift` | Cancel recording |
87
+ | **Voice Activity Detection** |||
88
+ | `vad.vad_precheck_enabled` | `true` | Prevent hallucinations on silence |
89
+ | `vad.vad_onset_threshold` | `0.7` | Speech detection start (0.0-1.0) |
90
+ | `vad.vad_offset_threshold` | `0.55` | Speech detection end (0.0-1.0) |
91
+ | `vad.vad_min_speech_duration` | `0.1` | Min speech segment (seconds) |
92
+ | `vad.vad_realtime_enabled` | `true` | Auto-stop on silence |
93
+ | `vad.vad_silence_timeout_seconds` | `30.0` | Seconds before auto-stop |
94
+ | **Audio** |||
95
+ | `audio.host` | `null` | Audio API (WASAPI, Core Audio, etc.) |
96
+ | `audio.channels` | `1` | 1 = mono, 2 = stereo |
97
+ | `audio.dtype` | `float32` | float32/int16/int24/int32 |
98
+ | `audio.max_duration` | `900` | Max recording seconds (0 = unlimited) |
99
+ | `audio.input_device` | `default` | Device ID or "default" |
100
+ | **Clipboard** |||
101
+ | `clipboard.auto_paste` | `true` | false = clipboard only |
102
+ | `clipboard.paste_hotkey` | `ctrl+v` / `cmd+v` | Paste key simulation |
103
+ | `clipboard.preserve_clipboard` | `true` | Restore clipboard after paste |
104
+ | `clipboard.key_simulation_delay` | `0.05` | Delay between keystrokes (seconds) |
105
+ | **Logging** |||
106
+ | `logging.level` | `INFO` | DEBUG/INFO/WARNING/ERROR/CRITICAL |
107
+ | `logging.file.enabled` | `true` | Write to app.log |
108
+ | `logging.console.enabled` | `true` | Print to console |
109
+ | `logging.console.level` | `WARNING` | Console verbosity |
110
+ | **Audio Feedback** |||
111
+ | `audio_feedback.enabled` | `true` | Play sounds on record/stop |
112
+ | `audio_feedback.start_sound` | `assets/sounds/...` | Custom sound file path |
113
+ | `audio_feedback.stop_sound` | `assets/sounds/...` | Custom sound file path |
114
+ | `audio_feedback.cancel_sound` | `assets/sounds/...` | Custom sound file path |
115
+ | **System Tray** |||
116
+ | `system_tray.enabled` | `true` | Show tray icon |
117
+ | `system_tray.tooltip` | `Whisper Key` | Hover text |
118
+ | **Console** |||
119
+ | `console.start_hidden` | `false` | Start minimized to tray |
120
+
121
+ ## 📁 Model Cache
122
+
123
+ Default path for transcription models (via HuggingFace):
124
+ - **Windows:** `%USERPROFILE%\.cache\huggingface\hub\`
125
+ - **macOS:** `~/.cache/huggingface/hub/`
126
+
127
+ ## 📦 Dependencies
128
+
129
+ **Cross-platform:**
130
+ `faster-whisper` · `numpy` · `sounddevice` · `soxr` · `pyperclip` · `ruamel.yaml` · `pystray` · `Pillow` · `playsound3` · `ten-vad` · `hf-xet`
131
+
132
+ **Windows:** `global-hotkeys` · `pywin32` · `pyautogui`
133
+
134
+ **macOS:** `pyobjc-framework-Quartz` · `pyobjc-framework-ApplicationServices`
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "whisper-key-local"
7
- version = "0.5.3"
7
+ version = "0.6.1"
8
8
  description = "Local faster-whisper speech-to-text app with global hotkeys for Windows"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -12,20 +12,28 @@ authors = [
12
12
  ]
13
13
  requires-python = ">=3.11"
14
14
  dependencies = [
15
+ # Cross-platform
15
16
  "faster-whisper>=1.2.1",
16
17
  "ctranslate2>=4.6.3",
17
18
  "numpy>=1.24.0",
18
19
  "soxr>=0.3.0",
19
20
  "sounddevice>=0.4.6",
20
- "global-hotkeys>=0.1.7; platform_system=='Windows'",
21
21
  "pyperclip>=1.8.2",
22
22
  "ruamel.yaml>=0.18.14",
23
- "pywin32>=306; platform_system=='Windows'",
24
- "pyautogui>=0.9.54; platform_system=='Windows'",
25
23
  "pystray>=0.19.5",
26
24
  "Pillow>=10.0.0",
27
25
  "hf-xet>=1.1.5",
28
- # Note: ten-vad git dependency handled separately
26
+ "playsound3>=2.0",
27
+ "ten-vad>=1.0.6",
28
+
29
+ # Windows-only
30
+ "global-hotkeys>=0.1.7; sys_platform=='win32'",
31
+ "pywin32>=306; sys_platform=='win32'",
32
+ "pyautogui>=0.9.54; sys_platform=='win32'",
33
+
34
+ # macOS-only
35
+ "pyobjc-framework-Quartz; sys_platform=='darwin'",
36
+ "pyobjc-framework-ApplicationServices; sys_platform=='darwin'",
29
37
  ]
30
38
 
31
39
  [project.scripts]
@@ -35,4 +43,4 @@ whisper-key = "whisper_key.main:main"
35
43
  where = ["src"]
36
44
 
37
45
  [tool.setuptools.package-data]
38
- "whisper_key" = ["assets/**/*", "config.defaults.yaml"]
46
+ "whisper_key" = ["assets/**/*", "platform/*/assets/*", "config.defaults.yaml"]
@@ -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()