whisper-key-local 0.3.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.3.0/PKG-INFO +124 -0
- whisper_key_local-0.3.0/README.md +105 -0
- whisper_key_local-0.3.0/pyproject.toml +36 -0
- whisper_key_local-0.3.0/setup.cfg +4 -0
- whisper_key_local-0.3.0/src/whisper_key/__init__.py +0 -0
- whisper_key_local-0.3.0/src/whisper_key/assets/sounds/record_cancel.wav +0 -0
- whisper_key_local-0.3.0/src/whisper_key/assets/sounds/record_start.wav +0 -0
- whisper_key_local-0.3.0/src/whisper_key/assets/sounds/record_stop.wav +0 -0
- whisper_key_local-0.3.0/src/whisper_key/assets/tray_idle.png +0 -0
- whisper_key_local-0.3.0/src/whisper_key/assets/tray_processing.png +0 -0
- whisper_key_local-0.3.0/src/whisper_key/assets/tray_recording.png +0 -0
- whisper_key_local-0.3.0/src/whisper_key/audio_feedback.py +56 -0
- whisper_key_local-0.3.0/src/whisper_key/audio_recorder.py +145 -0
- whisper_key_local-0.3.0/src/whisper_key/clipboard_manager.py +164 -0
- whisper_key_local-0.3.0/src/whisper_key/config.defaults.yaml +185 -0
- whisper_key_local-0.3.0/src/whisper_key/config_manager.py +382 -0
- whisper_key_local-0.3.0/src/whisper_key/hotkey_listener.py +202 -0
- whisper_key_local-0.3.0/src/whisper_key/instance_manager.py +36 -0
- whisper_key_local-0.3.0/src/whisper_key/main.py +194 -0
- whisper_key_local-0.3.0/src/whisper_key/state_manager.py +250 -0
- whisper_key_local-0.3.0/src/whisper_key/system_tray.py +238 -0
- whisper_key_local-0.3.0/src/whisper_key/utils.py +48 -0
- whisper_key_local-0.3.0/src/whisper_key/whisper_engine.py +312 -0
- whisper_key_local-0.3.0/src/whisper_key_local.egg-info/PKG-INFO +124 -0
- whisper_key_local-0.3.0/src/whisper_key_local.egg-info/SOURCES.txt +27 -0
- whisper_key_local-0.3.0/src/whisper_key_local.egg-info/dependency_links.txt +1 -0
- whisper_key_local-0.3.0/src/whisper_key_local.egg-info/entry_points.txt +2 -0
- whisper_key_local-0.3.0/src/whisper_key_local.egg-info/requires.txt +13 -0
- whisper_key_local-0.3.0/src/whisper_key_local.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: whisper-key-local
|
|
3
|
+
Version: 0.3.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.1.1
|
|
9
|
+
Requires-Dist: numpy>=1.24.0
|
|
10
|
+
Requires-Dist: sounddevice>=0.4.6
|
|
11
|
+
Requires-Dist: global-hotkeys>=0.1.7; platform_system == "Windows"
|
|
12
|
+
Requires-Dist: pyperclip>=1.8.2
|
|
13
|
+
Requires-Dist: ruamel.yaml>=0.18.14
|
|
14
|
+
Requires-Dist: pywin32>=306; platform_system == "Windows"
|
|
15
|
+
Requires-Dist: pyautogui>=0.9.54; platform_system == "Windows"
|
|
16
|
+
Requires-Dist: pystray>=0.19.5
|
|
17
|
+
Requires-Dist: Pillow>=10.0.0
|
|
18
|
+
Requires-Dist: hf-xet>=1.1.5
|
|
19
|
+
|
|
20
|
+
# Whisper Key - Local Speech-to-Text for Windows
|
|
21
|
+
|
|
22
|
+
Global hotkey to start/stop recording and auto-paste transcription wherever your cursor is.
|
|
23
|
+
|
|
24
|
+
## 🎯 Features
|
|
25
|
+
|
|
26
|
+
- **Global Hotkey**: Press `Ctrl+Win` to start recording, `Ctrl` to stop
|
|
27
|
+
- **Auto-Paste**: Transcription inserted at your cursor via auto-paste
|
|
28
|
+
- **Auto-Send**: Press `Alt` to stop, auto-paste, and auto-send with ENTER keypress
|
|
29
|
+
- **Offline Capable**: No internet required after models downloaded
|
|
30
|
+
- **Local Processing**: Voice data never leaves your computer
|
|
31
|
+
- **Configurable**: Customize hotkeys, model, transcription actions, and audio settings
|
|
32
|
+
|
|
33
|
+
## 🚀 Quick Start
|
|
34
|
+
|
|
35
|
+
### Prerequisites
|
|
36
|
+
- Windows 10/11
|
|
37
|
+
- Microphone
|
|
38
|
+
|
|
39
|
+
### Option 1: Download Portable App
|
|
40
|
+
1. [Download the latest release zip](https://github.com/PinW/whisper-key-local/releases/latest)
|
|
41
|
+
2. Extract the zip file
|
|
42
|
+
3. Run `whisper-key.exe`
|
|
43
|
+
|
|
44
|
+
### Option 2: Install via pipx
|
|
45
|
+
**Prerequisites:**
|
|
46
|
+
- Python 3.8+ installed on Windows
|
|
47
|
+
- pipx installed (`pip install pipx`)
|
|
48
|
+
|
|
49
|
+
Install the application globally:
|
|
50
|
+
```powershell
|
|
51
|
+
pipx install whisper-key
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Add VAD support (prevents silence hallucinations):
|
|
55
|
+
```powershell
|
|
56
|
+
pipx inject whisper-key git+https://github.com/TEN-framework/ten-vad.git@v1.0-ONNX
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Run from anywhere:
|
|
60
|
+
```powershell
|
|
61
|
+
whisper-key
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Option 3: Development Installation
|
|
65
|
+
**Prerequisites:**
|
|
66
|
+
- Python 3.8+ installed on Windows
|
|
67
|
+
|
|
68
|
+
Clone the repository:
|
|
69
|
+
```powershell
|
|
70
|
+
git clone https://github.com/PinW/whisper-key-local.git
|
|
71
|
+
cd whisper-key-local
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Install the dependencies:
|
|
75
|
+
```powershell
|
|
76
|
+
pip install -e .
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Add VAD support (prevents silence hallucinations):
|
|
80
|
+
```powershell
|
|
81
|
+
pip install git+https://github.com/TEN-framework/ten-vad.git@v1.0-ONNX
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Run the application:
|
|
85
|
+
```powershell
|
|
86
|
+
python whisper-key.py
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 📦 Package Dependencies
|
|
90
|
+
|
|
91
|
+
- `faster-whisper` - Fast AI speech recognition
|
|
92
|
+
- `numpy` - Numerical computing support
|
|
93
|
+
- `sounddevice` - Audio recording
|
|
94
|
+
- `global-hotkeys` - System-wide hotkey detection
|
|
95
|
+
- `pyperclip` - Clipboard operations
|
|
96
|
+
- `ruamel.yaml` - Configuration file parsing (YAML)
|
|
97
|
+
- `pyautogui` - Key simulation for Ctrl+V auto-paste and auto-ENTER
|
|
98
|
+
- `pywin32` - Windows API access for window management
|
|
99
|
+
- `pystray` - System tray integration
|
|
100
|
+
- `Pillow` - Image processing for system tray icons
|
|
101
|
+
- `hf-xet` - Cache management for Hugging Face models
|
|
102
|
+
- `ten-vad` - Voice Activity Detection to prevent silent hallucinations
|
|
103
|
+
|
|
104
|
+
## 🎮 Basic Usage
|
|
105
|
+
|
|
106
|
+
- Boot the app, the "tiny" model will download and start
|
|
107
|
+
- Press `Ctrl+Win` to start recording
|
|
108
|
+
- Speak into your microphone
|
|
109
|
+
- Press `Ctrl` to stop recording and transcribe
|
|
110
|
+
- The transcribed text is auto-pasted on your text cursor
|
|
111
|
+
- Alternatively press `Alt` to stop recording, and
|
|
112
|
+
- Right click the system tray icon to change models
|
|
113
|
+
|
|
114
|
+
### Configuration
|
|
115
|
+
The app automatically creates a user settings file in `%APPDATA%\Roaming\whisperkey\user_settings.yaml`, where you can:
|
|
116
|
+
- Change whisper model size (tiny/base/small/medium/large)
|
|
117
|
+
- Hotkeys
|
|
118
|
+
- Configure automation (auto-paste, auto-ENTER)
|
|
119
|
+
- And much more
|
|
120
|
+
|
|
121
|
+
## 🔧 Troubleshooting
|
|
122
|
+
|
|
123
|
+
- Check the log file `app.log` for detailed error messages
|
|
124
|
+
- Delete the user settings file in `%APPDATA%\Roaming\whisperkey\user_settings.yaml` and restart to reset to defaults
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Whisper Key - Local Speech-to-Text for Windows
|
|
2
|
+
|
|
3
|
+
Global hotkey to start/stop recording and auto-paste transcription wherever your cursor is.
|
|
4
|
+
|
|
5
|
+
## 🎯 Features
|
|
6
|
+
|
|
7
|
+
- **Global Hotkey**: Press `Ctrl+Win` to start recording, `Ctrl` to stop
|
|
8
|
+
- **Auto-Paste**: Transcription inserted at your cursor via auto-paste
|
|
9
|
+
- **Auto-Send**: Press `Alt` to stop, auto-paste, and auto-send with ENTER keypress
|
|
10
|
+
- **Offline Capable**: No internet required after models downloaded
|
|
11
|
+
- **Local Processing**: Voice data never leaves your computer
|
|
12
|
+
- **Configurable**: Customize hotkeys, model, transcription actions, and audio settings
|
|
13
|
+
|
|
14
|
+
## 🚀 Quick Start
|
|
15
|
+
|
|
16
|
+
### Prerequisites
|
|
17
|
+
- Windows 10/11
|
|
18
|
+
- Microphone
|
|
19
|
+
|
|
20
|
+
### Option 1: Download Portable App
|
|
21
|
+
1. [Download the latest release zip](https://github.com/PinW/whisper-key-local/releases/latest)
|
|
22
|
+
2. Extract the zip file
|
|
23
|
+
3. Run `whisper-key.exe`
|
|
24
|
+
|
|
25
|
+
### Option 2: Install via pipx
|
|
26
|
+
**Prerequisites:**
|
|
27
|
+
- Python 3.8+ installed on Windows
|
|
28
|
+
- pipx installed (`pip install pipx`)
|
|
29
|
+
|
|
30
|
+
Install the application globally:
|
|
31
|
+
```powershell
|
|
32
|
+
pipx install whisper-key
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Add VAD support (prevents silence hallucinations):
|
|
36
|
+
```powershell
|
|
37
|
+
pipx inject whisper-key git+https://github.com/TEN-framework/ten-vad.git@v1.0-ONNX
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Run from anywhere:
|
|
41
|
+
```powershell
|
|
42
|
+
whisper-key
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Option 3: Development Installation
|
|
46
|
+
**Prerequisites:**
|
|
47
|
+
- Python 3.8+ installed on Windows
|
|
48
|
+
|
|
49
|
+
Clone the repository:
|
|
50
|
+
```powershell
|
|
51
|
+
git clone https://github.com/PinW/whisper-key-local.git
|
|
52
|
+
cd whisper-key-local
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Install the dependencies:
|
|
56
|
+
```powershell
|
|
57
|
+
pip install -e .
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Add VAD support (prevents silence hallucinations):
|
|
61
|
+
```powershell
|
|
62
|
+
pip install git+https://github.com/TEN-framework/ten-vad.git@v1.0-ONNX
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Run the application:
|
|
66
|
+
```powershell
|
|
67
|
+
python whisper-key.py
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 📦 Package Dependencies
|
|
71
|
+
|
|
72
|
+
- `faster-whisper` - Fast AI speech recognition
|
|
73
|
+
- `numpy` - Numerical computing support
|
|
74
|
+
- `sounddevice` - Audio recording
|
|
75
|
+
- `global-hotkeys` - System-wide hotkey detection
|
|
76
|
+
- `pyperclip` - Clipboard operations
|
|
77
|
+
- `ruamel.yaml` - Configuration file parsing (YAML)
|
|
78
|
+
- `pyautogui` - Key simulation for Ctrl+V auto-paste and auto-ENTER
|
|
79
|
+
- `pywin32` - Windows API access for window management
|
|
80
|
+
- `pystray` - System tray integration
|
|
81
|
+
- `Pillow` - Image processing for system tray icons
|
|
82
|
+
- `hf-xet` - Cache management for Hugging Face models
|
|
83
|
+
- `ten-vad` - Voice Activity Detection to prevent silent hallucinations
|
|
84
|
+
|
|
85
|
+
## 🎮 Basic Usage
|
|
86
|
+
|
|
87
|
+
- Boot the app, the "tiny" model will download and start
|
|
88
|
+
- Press `Ctrl+Win` to start recording
|
|
89
|
+
- Speak into your microphone
|
|
90
|
+
- Press `Ctrl` to stop recording and transcribe
|
|
91
|
+
- The transcribed text is auto-pasted on your text cursor
|
|
92
|
+
- Alternatively press `Alt` to stop recording, and
|
|
93
|
+
- Right click the system tray icon to change models
|
|
94
|
+
|
|
95
|
+
### Configuration
|
|
96
|
+
The app automatically creates a user settings file in `%APPDATA%\Roaming\whisperkey\user_settings.yaml`, where you can:
|
|
97
|
+
- Change whisper model size (tiny/base/small/medium/large)
|
|
98
|
+
- Hotkeys
|
|
99
|
+
- Configure automation (auto-paste, auto-ENTER)
|
|
100
|
+
- And much more
|
|
101
|
+
|
|
102
|
+
## 🔧 Troubleshooting
|
|
103
|
+
|
|
104
|
+
- Check the log file `app.log` for detailed error messages
|
|
105
|
+
- Delete the user settings file in `%APPDATA%\Roaming\whisperkey\user_settings.yaml` and restart to reset to defaults
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "whisper-key-local"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Local faster-whisper speech-to-text app with global hotkeys for Windows"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{name = "Pin Wang", email = "pinwang@gmail.com"}
|
|
12
|
+
]
|
|
13
|
+
requires-python = ">=3.11"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"faster-whisper>=1.1.1",
|
|
16
|
+
"numpy>=1.24.0",
|
|
17
|
+
"sounddevice>=0.4.6",
|
|
18
|
+
"global-hotkeys>=0.1.7; platform_system=='Windows'",
|
|
19
|
+
"pyperclip>=1.8.2",
|
|
20
|
+
"ruamel.yaml>=0.18.14",
|
|
21
|
+
"pywin32>=306; platform_system=='Windows'",
|
|
22
|
+
"pyautogui>=0.9.54; platform_system=='Windows'",
|
|
23
|
+
"pystray>=0.19.5",
|
|
24
|
+
"Pillow>=10.0.0",
|
|
25
|
+
"hf-xet>=1.1.5",
|
|
26
|
+
# Note: ten-vad git dependency handled separately
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
whisper-key = "whisper_key.main:main"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
where = ["src"]
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.package-data]
|
|
36
|
+
"whisper_key" = ["assets/**/*", "config.defaults.yaml"]
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import threading
|
|
4
|
+
import winsound
|
|
5
|
+
|
|
6
|
+
from .utils import resolve_asset_path
|
|
7
|
+
|
|
8
|
+
class AudioFeedback:
|
|
9
|
+
def __init__(self, enabled=True, start_sound='', stop_sound='', cancel_sound=''):
|
|
10
|
+
self.enabled = enabled
|
|
11
|
+
self.logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
self.start_sound_path = resolve_asset_path(start_sound)
|
|
14
|
+
self.stop_sound_path = resolve_asset_path(stop_sound)
|
|
15
|
+
self.cancel_sound_path = resolve_asset_path(cancel_sound)
|
|
16
|
+
|
|
17
|
+
if not self.enabled:
|
|
18
|
+
self.logger.info("Audio feedback disabled by configuration")
|
|
19
|
+
print(" ✗ Audio feedback disabled")
|
|
20
|
+
else:
|
|
21
|
+
self._validate_sound_files()
|
|
22
|
+
print(" ✓ Audio feedback enabled...")
|
|
23
|
+
|
|
24
|
+
def _validate_sound_files(self):
|
|
25
|
+
if self.start_sound_path and not os.path.isfile(self.start_sound_path):
|
|
26
|
+
self.logger.warning(f"Start sound file not found: {self.start_sound_path}")
|
|
27
|
+
|
|
28
|
+
if self.stop_sound_path and not os.path.isfile(self.stop_sound_path):
|
|
29
|
+
self.logger.warning(f"Stop sound file not found: {self.stop_sound_path}")
|
|
30
|
+
|
|
31
|
+
if self.cancel_sound_path and not os.path.isfile(self.cancel_sound_path):
|
|
32
|
+
self.logger.warning(f"Cancel sound file not found: {self.cancel_sound_path}")
|
|
33
|
+
|
|
34
|
+
def _play_sound_file_async(self, file_path: str):
|
|
35
|
+
def play_sound():
|
|
36
|
+
try:
|
|
37
|
+
# SND_FILENAME = play from file, SND_ASYNC = don't block
|
|
38
|
+
winsound.PlaySound(file_path, winsound.SND_FILENAME | winsound.SND_ASYNC)
|
|
39
|
+
|
|
40
|
+
except Exception as e:
|
|
41
|
+
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
|
+
|
|
46
|
+
def play_start_sound(self):
|
|
47
|
+
if self.enabled:
|
|
48
|
+
self._play_sound_file_async(self.start_sound_path)
|
|
49
|
+
|
|
50
|
+
def play_stop_sound(self):
|
|
51
|
+
if self.enabled:
|
|
52
|
+
self._play_sound_file_async(self.stop_sound_path)
|
|
53
|
+
|
|
54
|
+
def play_cancel_sound(self):
|
|
55
|
+
if self.enabled:
|
|
56
|
+
self._play_sound_file_async(self.cancel_sound_path)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import sounddevice as sd
|
|
8
|
+
|
|
9
|
+
class AudioRecorder:
|
|
10
|
+
WHISPER_SAMPLE_RATE = 16000
|
|
11
|
+
THREAD_JOIN_TIMEOUT = 2.0
|
|
12
|
+
RECORDING_SLEEP_INTERVAL = 100
|
|
13
|
+
STREAM_DTYPE = np.float32
|
|
14
|
+
|
|
15
|
+
def __init__(self,
|
|
16
|
+
channels: int = 1,
|
|
17
|
+
dtype: str = "float32",
|
|
18
|
+
max_duration: int = 30,
|
|
19
|
+
on_max_duration_reached: callable = None):
|
|
20
|
+
|
|
21
|
+
self.sample_rate = self.WHISPER_SAMPLE_RATE
|
|
22
|
+
self.channels = channels
|
|
23
|
+
self.dtype = dtype
|
|
24
|
+
self.max_duration = max_duration
|
|
25
|
+
self.on_max_duration_reached = on_max_duration_reached
|
|
26
|
+
self.is_recording = False
|
|
27
|
+
self.audio_data = []
|
|
28
|
+
self.recording_thread = None
|
|
29
|
+
self.recording_start_time = None
|
|
30
|
+
self.logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
self._test_microphone()
|
|
33
|
+
|
|
34
|
+
def _wait_for_thread_finish(self):
|
|
35
|
+
if self.recording_thread:
|
|
36
|
+
self.recording_thread.join(timeout=self.THREAD_JOIN_TIMEOUT)
|
|
37
|
+
|
|
38
|
+
def _test_microphone(self):
|
|
39
|
+
try:
|
|
40
|
+
default_input = sd.query_devices(kind='input')
|
|
41
|
+
self.logger.info(f"Default microphone: {default_input['name']}")
|
|
42
|
+
|
|
43
|
+
except Exception as e:
|
|
44
|
+
self.logger.error(f"Microphone test failed: {e}")
|
|
45
|
+
raise
|
|
46
|
+
|
|
47
|
+
def start_recording(self):
|
|
48
|
+
if self.is_recording:
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
self.logger.info("Starting audio recording...")
|
|
53
|
+
self.is_recording = True
|
|
54
|
+
self.audio_data = []
|
|
55
|
+
self.recording_start_time = time.time()
|
|
56
|
+
|
|
57
|
+
self.recording_thread = threading.Thread(target=self._record_audio)
|
|
58
|
+
self.recording_thread.daemon = True # Thread will close when main program closes
|
|
59
|
+
self.recording_thread.start()
|
|
60
|
+
|
|
61
|
+
print("🎤 Recording started! Speak now...")
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
self.logger.error(f"Failed to start audio recording: {e}")
|
|
66
|
+
print("❌ Failed to start recording!")
|
|
67
|
+
self.is_recording = False
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
def stop_recording(self) -> Optional[np.ndarray]:
|
|
71
|
+
if not self.is_recording:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
self.is_recording = False
|
|
75
|
+
self._wait_for_thread_finish()
|
|
76
|
+
|
|
77
|
+
return self._process_audio_data()
|
|
78
|
+
|
|
79
|
+
def _process_audio_data(self) -> Optional[np.ndarray]:
|
|
80
|
+
if len(self.audio_data) == 0:
|
|
81
|
+
print(" ✗ No audio data recorded!")
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
# Convert list of audio chunks into a single numpy array
|
|
85
|
+
audio_array = np.concatenate(self.audio_data, axis=0)
|
|
86
|
+
duration = self.get_audio_duration(audio_array)
|
|
87
|
+
self.logger.info(f"Recorded {duration:.2f} seconds of audio")
|
|
88
|
+
return audio_array
|
|
89
|
+
|
|
90
|
+
def cancel_recording(self):
|
|
91
|
+
if not self.is_recording:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
self.is_recording = False
|
|
95
|
+
self._wait_for_thread_finish()
|
|
96
|
+
|
|
97
|
+
self.audio_data = []
|
|
98
|
+
self.recording_start_time = None
|
|
99
|
+
|
|
100
|
+
def _record_audio(self):
|
|
101
|
+
try:
|
|
102
|
+
def audio_callback(audio_data, frames, time, status):
|
|
103
|
+
if self.is_recording:
|
|
104
|
+
self.audio_data.append(audio_data.copy())
|
|
105
|
+
|
|
106
|
+
if status:
|
|
107
|
+
self.logger.debug(f"Audio callback status: {status}")
|
|
108
|
+
|
|
109
|
+
with sd.InputStream(samplerate=self.sample_rate,
|
|
110
|
+
channels=self.channels,
|
|
111
|
+
callback=audio_callback,
|
|
112
|
+
dtype=self.STREAM_DTYPE):
|
|
113
|
+
|
|
114
|
+
while self.is_recording:
|
|
115
|
+
if self._check_max_duration_exceeded():
|
|
116
|
+
break
|
|
117
|
+
|
|
118
|
+
sd.sleep(self.RECORDING_SLEEP_INTERVAL)
|
|
119
|
+
|
|
120
|
+
except Exception as e:
|
|
121
|
+
self.logger.error(f"Error during audio recording: {e}")
|
|
122
|
+
self.is_recording = False
|
|
123
|
+
|
|
124
|
+
def _check_max_duration_exceeded(self) -> bool:
|
|
125
|
+
if self.max_duration > 0 and self.recording_start_time:
|
|
126
|
+
elapsed_time = time.time() - self.recording_start_time
|
|
127
|
+
if elapsed_time >= self.max_duration:
|
|
128
|
+
self.logger.info(f"Maximum recording duration of {self.max_duration}s reached")
|
|
129
|
+
print(f"⏰ Maximum recording duration of {self.max_duration}s reached - stopping recording")
|
|
130
|
+
|
|
131
|
+
self.is_recording = False
|
|
132
|
+
audio_data = self._process_audio_data()
|
|
133
|
+
|
|
134
|
+
if self.on_max_duration_reached:
|
|
135
|
+
self.on_max_duration_reached(audio_data)
|
|
136
|
+
return True
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
def get_recording_status(self) -> bool:
|
|
140
|
+
return self.is_recording
|
|
141
|
+
|
|
142
|
+
def get_audio_duration(self, audio_data: np.ndarray) -> float:
|
|
143
|
+
if audio_data is None or len(audio_data) == 0:
|
|
144
|
+
return 0.0
|
|
145
|
+
return len(audio_data) / self.sample_rate
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import pyperclip
|
|
6
|
+
import win32gui
|
|
7
|
+
import pyautogui
|
|
8
|
+
|
|
9
|
+
pyautogui.FAILSAFE = True # Enable "move mouse to corner to abort automation"
|
|
10
|
+
|
|
11
|
+
class ClipboardManager:
|
|
12
|
+
def __init__(self, key_simulation_delay, auto_paste, preserve_clipboard):
|
|
13
|
+
self.logger = logging.getLogger(__name__)
|
|
14
|
+
self.key_simulation_delay = key_simulation_delay
|
|
15
|
+
self.auto_paste = auto_paste
|
|
16
|
+
self.preserve_clipboard = preserve_clipboard
|
|
17
|
+
self._configure_pyautogui_timing()
|
|
18
|
+
self._test_clipboard_access()
|
|
19
|
+
self._print_status()
|
|
20
|
+
|
|
21
|
+
def _configure_pyautogui_timing(self):
|
|
22
|
+
pyautogui.PAUSE = self.key_simulation_delay
|
|
23
|
+
|
|
24
|
+
def _test_clipboard_access(self):
|
|
25
|
+
try:
|
|
26
|
+
pyperclip.paste()
|
|
27
|
+
self.logger.info("Clipboard access test successful")
|
|
28
|
+
|
|
29
|
+
except Exception as e:
|
|
30
|
+
self.logger.error(f"Clipboard access test failed: {e}")
|
|
31
|
+
raise
|
|
32
|
+
|
|
33
|
+
def _print_status(self):
|
|
34
|
+
if self.auto_paste:
|
|
35
|
+
method_name = "key simulation (CTRL+V)"
|
|
36
|
+
print(f" ✓ Auto-paste is ENABLED using {method_name}")
|
|
37
|
+
else:
|
|
38
|
+
print(" ✗ Auto-paste is DISABLED - paste manually with Ctrl+V")
|
|
39
|
+
|
|
40
|
+
def copy_text(self, text: str) -> bool:
|
|
41
|
+
if not text:
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
self.logger.info(f"Copying text to clipboard ({len(text)} chars)")
|
|
46
|
+
pyperclip.copy(text)
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
except Exception as e:
|
|
50
|
+
self.logger.error(f"Failed to copy text to clipboard: {e}")
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
def get_clipboard_content(self) -> Optional[str]:
|
|
54
|
+
try:
|
|
55
|
+
clipboard_content = pyperclip.paste()
|
|
56
|
+
|
|
57
|
+
if clipboard_content:
|
|
58
|
+
return clipboard_content
|
|
59
|
+
else:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
except Exception as e:
|
|
63
|
+
self.logger.error(f"Failed to paste text from clipboard: {e}")
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
def copy_with_notification(self, text: str) -> bool:
|
|
67
|
+
if not text:
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
success = self.copy_text(text)
|
|
71
|
+
|
|
72
|
+
if success:
|
|
73
|
+
print(" ✓ Copied to clipboard")
|
|
74
|
+
print(" ✓ You can now paste with Ctrl+V in any application!")
|
|
75
|
+
|
|
76
|
+
return success
|
|
77
|
+
|
|
78
|
+
def clear_clipboard(self) -> bool:
|
|
79
|
+
try:
|
|
80
|
+
pyperclip.copy("")
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
self.logger.error(f"Failed to clear clipboard: {e}")
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
def get_active_window_handle(self) -> Optional[int]:
|
|
88
|
+
try:
|
|
89
|
+
hwnd = win32gui.GetForegroundWindow()
|
|
90
|
+
if hwnd:
|
|
91
|
+
window_title = win32gui.GetWindowText(hwnd)
|
|
92
|
+
self.logger.info(f"Active window: '{window_title}' (handle: {hwnd})")
|
|
93
|
+
return hwnd
|
|
94
|
+
else:
|
|
95
|
+
return None
|
|
96
|
+
except Exception as e:
|
|
97
|
+
self.logger.error(f"Failed to get active window handle: {e}")
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def execute_auto_paste(self, text: str, preserve_clipboard: bool) -> bool:
|
|
101
|
+
try:
|
|
102
|
+
original_content = None
|
|
103
|
+
if preserve_clipboard:
|
|
104
|
+
original_content = pyperclip.paste()
|
|
105
|
+
|
|
106
|
+
if not self.copy_text(text):
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
pyautogui.hotkey('ctrl', 'v')
|
|
110
|
+
|
|
111
|
+
print(f" ✓ Auto-pasted via key simulation")
|
|
112
|
+
|
|
113
|
+
if original_content is not None:
|
|
114
|
+
pyperclip.copy(original_content)
|
|
115
|
+
time.sleep(self.key_simulation_delay)
|
|
116
|
+
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
self.logger.error(f"Failed to simulate paste keypress: {e}")
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
def send_enter_key(self) -> bool:
|
|
124
|
+
try:
|
|
125
|
+
self.logger.info("Sending ENTER key to active application")
|
|
126
|
+
pyautogui.press('enter')
|
|
127
|
+
print(" ✓ Text submitted with ENTER!")
|
|
128
|
+
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
except Exception as e:
|
|
132
|
+
self.logger.error(f"Failed to send ENTER key: {e}")
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
def deliver_transcription(self,
|
|
136
|
+
transcribed_text: str,
|
|
137
|
+
use_auto_enter: bool = False) -> bool:
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
if use_auto_enter:
|
|
141
|
+
print("🚀 Auto-pasting text and SENDING with ENTER...")
|
|
142
|
+
|
|
143
|
+
# Force auto-paste when using auto-enter hotkey
|
|
144
|
+
success = self.execute_auto_paste(transcribed_text, self.preserve_clipboard)
|
|
145
|
+
if success:
|
|
146
|
+
success = self.send_enter_key()
|
|
147
|
+
|
|
148
|
+
elif self.auto_paste:
|
|
149
|
+
print("🚀 Auto-pasting text...")
|
|
150
|
+
success = self.execute_auto_paste(transcribed_text, self.preserve_clipboard)
|
|
151
|
+
|
|
152
|
+
else:
|
|
153
|
+
print("📋 Copying to clipboard...")
|
|
154
|
+
success = self.copy_with_notification(transcribed_text)
|
|
155
|
+
|
|
156
|
+
return success
|
|
157
|
+
|
|
158
|
+
except Exception as e:
|
|
159
|
+
self.logger.error(f"Delivery workflow failed: {e}")
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
def update_auto_paste(self, enabled: bool):
|
|
163
|
+
self.auto_paste = enabled
|
|
164
|
+
self._print_status()
|