whisper-key-local 0.5.3__tar.gz → 0.6.0__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.
- whisper_key_local-0.6.0/PKG-INFO +159 -0
- whisper_key_local-0.6.0/README.md +134 -0
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/pyproject.toml +13 -5
- whisper_key_local-0.6.0/src/whisper_key/assets/version.txt +1 -0
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key/audio_feedback.py +21 -20
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key/clipboard_manager.py +35 -52
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key/config.defaults.yaml +22 -13
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key/config_manager.py +25 -9
- whisper_key_local-0.6.0/src/whisper_key/console_manager.py +3 -0
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key/hotkey_listener.py +55 -89
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key/instance_manager.py +9 -13
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key/main.py +28 -11
- whisper_key_local-0.6.0/src/whisper_key/platform/__init__.py +10 -0
- whisper_key_local-0.6.0/src/whisper_key/platform/macos/__init__.py +1 -0
- whisper_key_local-0.6.0/src/whisper_key/platform/macos/app.py +27 -0
- whisper_key_local-0.6.0/src/whisper_key/platform/macos/console.py +13 -0
- whisper_key_local-0.6.0/src/whisper_key/platform/macos/hotkeys.py +180 -0
- whisper_key_local-0.6.0/src/whisper_key/platform/macos/icons.py +11 -0
- whisper_key_local-0.6.0/src/whisper_key/platform/macos/instance_lock.py +31 -0
- whisper_key_local-0.6.0/src/whisper_key/platform/macos/keyboard.py +97 -0
- whisper_key_local-0.6.0/src/whisper_key/platform/macos/keycodes.py +17 -0
- whisper_key_local-0.6.0/src/whisper_key/platform/macos/paths.py +8 -0
- whisper_key_local-0.6.0/src/whisper_key/platform/macos/permissions.py +66 -0
- whisper_key_local-0.6.0/src/whisper_key/platform/windows/__init__.py +1 -0
- whisper_key_local-0.6.0/src/whisper_key/platform/windows/app.py +6 -0
- whisper_key_local-0.5.3/src/whisper_key/console_manager.py → whisper_key_local-0.6.0/src/whisper_key/platform/windows/console.py +4 -4
- whisper_key_local-0.6.0/src/whisper_key/platform/windows/hotkeys.py +30 -0
- whisper_key_local-0.6.0/src/whisper_key/platform/windows/icons.py +11 -0
- whisper_key_local-0.6.0/src/whisper_key/platform/windows/instance_lock.py +14 -0
- whisper_key_local-0.6.0/src/whisper_key/platform/windows/keyboard.py +12 -0
- whisper_key_local-0.6.0/src/whisper_key/platform/windows/paths.py +8 -0
- whisper_key_local-0.6.0/src/whisper_key/platform/windows/permissions.py +6 -0
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key/state_manager.py +4 -4
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key/system_tray.py +30 -57
- whisper_key_local-0.6.0/src/whisper_key/terminal_ui.py +71 -0
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key/utils.py +8 -4
- whisper_key_local-0.6.0/src/whisper_key_local.egg-info/PKG-INFO +159 -0
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key_local.egg-info/SOURCES.txt +21 -3
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key_local.egg-info/requires.txt +7 -1
- whisper_key_local-0.5.3/PKG-INFO +0 -130
- whisper_key_local-0.5.3/README.md +0 -109
- whisper_key_local-0.5.3/src/whisper_key/assets/tray_idle.png +0 -0
- whisper_key_local-0.5.3/src/whisper_key/assets/tray_processing.png +0 -0
- whisper_key_local-0.5.3/src/whisper_key/assets/tray_recording.png +0 -0
- whisper_key_local-0.5.3/src/whisper_key/assets/version.txt +0 -1
- whisper_key_local-0.5.3/src/whisper_key_local.egg-info/PKG-INFO +0 -130
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/setup.cfg +0 -0
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key/__init__.py +0 -0
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key/assets/portaudio.dll +0 -0
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key/assets/sounds/record_cancel.wav +0 -0
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key/assets/sounds/record_start.wav +0 -0
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key/assets/sounds/record_stop.wav +0 -0
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key/audio_recorder.py +0 -0
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key/model_registry.py +0 -0
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key/voice_activity_detection.py +0 -0
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key/whisper_engine.py +0 -0
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key_local.egg-info/dependency_links.txt +0 -0
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/src/whisper_key_local.egg-info/entry_points.txt +0 -0
- {whisper_key_local-0.5.3 → whisper_key_local-0.6.0}/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.0
|
|
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.
|
|
7
|
+
version = "0.6.0"
|
|
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
|
-
|
|
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]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.6.0
|
|
@@ -1,56 +1,57 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
+
import platform
|
|
3
4
|
import threading
|
|
4
|
-
|
|
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
|
|
39
|
+
def play():
|
|
36
40
|
try:
|
|
37
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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.
|
|
18
|
+
self._configure_keyboard_timing()
|
|
22
19
|
self._test_clipboard_access()
|
|
23
20
|
self._print_status()
|
|
24
|
-
|
|
25
|
-
def
|
|
26
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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()
|