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.
Files changed (29) hide show
  1. whisper_key_local-0.3.0/PKG-INFO +124 -0
  2. whisper_key_local-0.3.0/README.md +105 -0
  3. whisper_key_local-0.3.0/pyproject.toml +36 -0
  4. whisper_key_local-0.3.0/setup.cfg +4 -0
  5. whisper_key_local-0.3.0/src/whisper_key/__init__.py +0 -0
  6. whisper_key_local-0.3.0/src/whisper_key/assets/sounds/record_cancel.wav +0 -0
  7. whisper_key_local-0.3.0/src/whisper_key/assets/sounds/record_start.wav +0 -0
  8. whisper_key_local-0.3.0/src/whisper_key/assets/sounds/record_stop.wav +0 -0
  9. whisper_key_local-0.3.0/src/whisper_key/assets/tray_idle.png +0 -0
  10. whisper_key_local-0.3.0/src/whisper_key/assets/tray_processing.png +0 -0
  11. whisper_key_local-0.3.0/src/whisper_key/assets/tray_recording.png +0 -0
  12. whisper_key_local-0.3.0/src/whisper_key/audio_feedback.py +56 -0
  13. whisper_key_local-0.3.0/src/whisper_key/audio_recorder.py +145 -0
  14. whisper_key_local-0.3.0/src/whisper_key/clipboard_manager.py +164 -0
  15. whisper_key_local-0.3.0/src/whisper_key/config.defaults.yaml +185 -0
  16. whisper_key_local-0.3.0/src/whisper_key/config_manager.py +382 -0
  17. whisper_key_local-0.3.0/src/whisper_key/hotkey_listener.py +202 -0
  18. whisper_key_local-0.3.0/src/whisper_key/instance_manager.py +36 -0
  19. whisper_key_local-0.3.0/src/whisper_key/main.py +194 -0
  20. whisper_key_local-0.3.0/src/whisper_key/state_manager.py +250 -0
  21. whisper_key_local-0.3.0/src/whisper_key/system_tray.py +238 -0
  22. whisper_key_local-0.3.0/src/whisper_key/utils.py +48 -0
  23. whisper_key_local-0.3.0/src/whisper_key/whisper_engine.py +312 -0
  24. whisper_key_local-0.3.0/src/whisper_key_local.egg-info/PKG-INFO +124 -0
  25. whisper_key_local-0.3.0/src/whisper_key_local.egg-info/SOURCES.txt +27 -0
  26. whisper_key_local-0.3.0/src/whisper_key_local.egg-info/dependency_links.txt +1 -0
  27. whisper_key_local-0.3.0/src/whisper_key_local.egg-info/entry_points.txt +2 -0
  28. whisper_key_local-0.3.0/src/whisper_key_local.egg-info/requires.txt +13 -0
  29. 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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -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()