forked from ILikeAI/AlwaysReddy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.py
248 lines (214 loc) · 10.2 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
import time
import threading
from audio_recorder import AudioRecorder
from transcription_manager import TranscriptionManager
from input_apis.input_handler import get_input_handler
import tts_manager
from completion_manager import CompletionManager
from soundfx import play_sound_FX
from utils import read_clipboard
from config_loader import config
import os
import importlib
from actions.base_action import BaseAction
# Import actions
class AlwaysReddy:
def __init__(self):
"""Initialize the AlwaysReddy instance with default settings and objects."""
self.verbose = config.VERBOSE
self.recorder = AudioRecorder(verbose=self.verbose)
self.clipboard_text = None
self.last_clipboard_text = None
self.tts = tts_manager.TTSManager(parent_client=self, verbose=self.verbose)
self.recording_timeout_timer = None
self.transcription_manager = TranscriptionManager(verbose=self.verbose)
self.completion_client = CompletionManager(verbose=self.verbose)
self.action_thread = None
self.stop_action = False
self.input_handler = get_input_handler(verbose=self.verbose)
self.input_handler.double_tap_threshold = config.DOUBLE_TAP_THRESHOLD
self.last_action_time = 0
self.current_recording_action = None
def _start_recording(self, action=None):
"""
Start the audio recording process and set a timeout for automatic stopping.
Args:
action (callable, optional): The action to be called when the recording times out.
"""
if self.verbose:
print(f"Starting recording... Action: {action.__name__ if action else 'None'}")
play_sound_FX("start", volume=config.START_SOUND_VOLUME, verbose=self.verbose)
self.recorder.start_recording()
self.current_recording_action = action
self.recording_timeout_timer = threading.Timer(config.MAX_RECORDING_DURATION, self._handle_recording_timeout)
self.recording_timeout_timer.start()
def _stop_recording(self):
"""Stop the current recording and return the filename."""
self._cancel_recording_timeout_timer()
if self.verbose:
print("Stopping recording...")
play_sound_FX("end", volume=config.END_SOUND_VOLUME, verbose=self.verbose)
return self.recorder.stop_recording()
def _handle_recording_timeout(self):
"""Handle the recording timeout by stopping the recording and calling the current recording action."""
if self.verbose:
print("Recording timeout reached.")
if self.current_recording_action:
if self.verbose:
print(f"Attempting to run {self.current_recording_action.__name__}")
self.execute_action_in_thread(self.current_recording_action)
else:
if self.verbose:
print("No action set for recording timeout.")
self.current_recording_action = None # Clear the action after execution
def _cancel_recording_timeout_timer(self):
"""Cancel the recording timeout timer if it is running."""
if self.recording_timeout_timer and self.recording_timeout_timer.is_alive():
self.recording_timeout_timer.cancel()
def _cancel_recording(self):
"""Cancel the current recording."""
if self.recorder.recording:
if self.verbose:
print("Cancelling recording...")
self.recorder.stop_recording(cancel=True)
if self.verbose:
print("Recording cancelled.")
def _cancel_tts(self):
"""Cancel the current TTS."""
if self.verbose:
print("Stopping text-to-speech...")
self.tts.stop()
if self.verbose:
print("Text-to-speech cancelled.")
def cancel_all(self, silent=False):
"""
Cancel the current recording and TTS.
This method runs outside the main action thread and can be called at any time to stop ongoing processes.
Args:
silent (bool): If True, don't play the cancel sound.
"""
cancelled_something = False
self._cancel_recording_timeout_timer()
if self.action_thread is not None and self.action_thread.is_alive():
self.stop_action = True
cancelled_something = True
if self.recorder.recording:
self._cancel_recording()
cancelled_something = True
if self.tts.running_tts:
self._cancel_tts()
cancelled_something = True
if cancelled_something and not silent:
play_sound_FX("cancel", volume=config.CANCEL_SOUND_VOLUME, verbose=self.verbose)
def add_action_hotkey(self, hotkey, *, pressed=None, released=None, held=None, held_release=None, double_tap=None, run_in_action_thread=True):
"""
Add a hotkey for an action with specified callbacks for different events.
Args:
hotkey (str): The hotkey combination.
pressed (callable, optional): Callback for when the hotkey is pressed.
released (callable, optional): Callback for when the hotkey is released.
held (callable, optional): Callback for when the hotkey is held.
held_release (callable, optional): Callback for when the hotkey is released after being held.
double_tap (callable, optional): Callback for when the hotkey is double-tapped.
run_in_action_thread (bool): If True, the action will run in the main thread. Default is True.
"""
def wrap_for_action_thread(method):
if method is None:
return None
def run_in_action_thread():
self.execute_action_in_thread(method)
return run_in_action_thread
wrapped_kwargs = {}
for event, method in [('pressed', pressed), ('released', released), ('held', held),
('held_release', held_release), ('double_tap', double_tap)]:
if method is not None:
wrapped_kwargs[event] = wrap_for_action_thread(method) if run_in_action_thread else method
self.input_handler.add_hotkey(hotkey, **wrapped_kwargs)
def toggle_recording(self, action=None):
"""
Handle the hotkey press for starting or stopping recording.
Args:
action (callable, optional): The action to be called when the recording is stopped.
Returns:
str or None: The recording filename if stopped, None if started.
"""
if self.recorder.recording:
self.stop_action = False
filename = self._stop_recording()
return filename
else:
if config.ALWAYS_INCLUDE_CLIPBOARD:
self.save_clipboard_text()
self._start_recording(action)
return None
def execute_action_in_thread(self, action_to_run, *args, **kwargs):
"""
Execute an action in a separate thread.
Args:
action_to_run (callable): The action to be executed.
*args: Positional arguments for the action.
**kwargs: Keyword arguments for the action.
"""
current_time = time.time()
if current_time - self.last_action_time < 0.1: # Delay between actions
print("Action triggered too quickly. Please wait.")
return
self.last_action_time = current_time
if self.action_thread is not None and self.action_thread.is_alive():
self.cancel_all(silent=True)
self.action_thread.join(timeout=2) # Wait for up to 2 seconds
if self.action_thread.is_alive():
print("Warning: Previous action did not end properly.")
if self.verbose:
print(f"Running {action_to_run.__name__}...")
self.stop_action = False
self.action_thread = threading.Thread(target=action_to_run, args=args, kwargs=kwargs)
self.action_thread.start()
def save_clipboard_text(self):
"""Save the current clipboard text."""
try:
print("Saving clipboard text...")
self.clipboard_text = read_clipboard()
except Exception as e:
if self.verbose:
print(f"Error saving clipboard text: {e}")
def discover_and_initialize_actions(self):
actions_dir = 'actions'
for action_folder in os.listdir(actions_dir):
# Skip the example_action folder
if action_folder == 'example_action':
continue
folder_path = os.path.join(actions_dir, action_folder)
if os.path.isdir(folder_path):
main_file = os.path.join(folder_path, 'main.py')
if os.path.exists(main_file):
module_name = f'actions.{action_folder}.main'
module = importlib.import_module(module_name)
for name, obj in module.__dict__.items():
if isinstance(obj, type) and issubclass(obj, BaseAction) and obj is not BaseAction:
print(f"\nInitializing action: {obj.__name__}")
action_instance = obj(self)
def run(self):
"""Run the AlwaysReddy instance, setting up hotkeys and entering the main loop."""
print("\n\nSetting up AlwaysReddy...\n")
self.discover_and_initialize_actions()
print("\nSystem actions:")
# Add cancel_all as an action that doesn't run in the main thread
self.add_action_hotkey(config.CANCEL_HOTKEY, pressed=self.cancel_all, run_in_action_thread=False)
print(f"'{config.CANCEL_HOTKEY}': Cancel currently running action, recording, TTS or other")
print("\nAlwaysReddy is reddy. Use any of the hotkeys above to get started.")
try:
self.input_handler.start(blocking=True)
except KeyboardInterrupt:
print("\nShutting down AlwaysReddy...")
finally:
self.cancel_all(silent=True)
if __name__ == "__main__":
try:
AlwaysReddy().run()
except Exception as e:
if config.VERBOSE:
import traceback
traceback.print_exc()
else:
print(f"Failed to start AlwaysReddy: {e}")