diff --git a/utils/__pycache__/api_key_manager.cpython-312.pyc b/utils/__pycache__/api_key_manager.cpython-312.pyc new file mode 100644 index 0000000..0318616 Binary files /dev/null and b/utils/__pycache__/api_key_manager.cpython-312.pyc differ diff --git a/utils/__pycache__/hotkey_manager.cpython-312.pyc b/utils/__pycache__/hotkey_manager.cpython-312.pyc new file mode 100644 index 0000000..db3080a Binary files /dev/null and b/utils/__pycache__/hotkey_manager.cpython-312.pyc differ diff --git a/utils/__pycache__/resource_utils.cpython-312.pyc b/utils/__pycache__/resource_utils.cpython-312.pyc new file mode 100644 index 0000000..c7e4a78 Binary files /dev/null and b/utils/__pycache__/resource_utils.cpython-312.pyc differ diff --git a/utils/__pycache__/text_to_mic.cpython-312.pyc b/utils/__pycache__/text_to_mic.cpython-312.pyc index 1e40b52..162b6c7 100644 Binary files a/utils/__pycache__/text_to_mic.cpython-312.pyc and b/utils/__pycache__/text_to_mic.cpython-312.pyc differ diff --git a/utils/api_key_manager.py b/utils/api_key_manager.py new file mode 100644 index 0000000..86be346 --- /dev/null +++ b/utils/api_key_manager.py @@ -0,0 +1,89 @@ +import os +import platform +from pathlib import Path +from tkinter import messagebox, simpledialog +from dotenv import load_dotenv + +class APIKeyManager: + """Class to handle API key management operations.""" + + @staticmethod + def get_app_support_path_mac(): + """Get the application support path for macOS.""" + home = Path.home() + app_support_path = home / 'Library' / 'Application Support' / 'scorchsoft-text-to-mic' + app_support_path.mkdir(parents=True, exist_ok=True) # Ensure directory exists + return app_support_path + + @staticmethod + def save_api_key_mac(api_key): + """Save the API key on macOS.""" + env_path = APIKeyManager.get_app_support_path_mac() / 'config' / '.env' + env_path.parent.mkdir(parents=True, exist_ok=True) # Ensure config directory exists + with open(env_path, 'w') as f: + f.write(f"OPENAI_API_KEY={api_key}\n") + + @staticmethod + def save_api_key(api_key): + """Save the API key to the config/.env file.""" + try: + config_dir = Path("config") + config_dir.mkdir(parents=True, exist_ok=True) # Ensure directory exists + + env_path = config_dir / ".env" + with open(env_path, 'w') as f: + f.write(f"OPENAI_API_KEY={api_key}\n") + + # Reload environment to include the new API key + load_dotenv(dotenv_path=env_path) + return True + except Exception as e: + messagebox.showerror("Error", f"Failed to save API key: {str(e)}") + return False + + @staticmethod + def load_api_key_mac(): + """Load the API key on macOS.""" + env_path = APIKeyManager.get_app_support_path_mac() / 'config' / '.env' + if env_path.exists(): + with open(env_path, 'r') as f: + for line in f: + if line.startswith('OPENAI_API_KEY'): + return line.strip().split('=')[1] + return None + + @staticmethod + def get_api_key(parent=None): + """Get the API key from environment variables or local file, or prompt the user.""" + # First, try to load the API key from environment variables or local file + api_key = os.getenv("OPENAI_API_KEY") + if not api_key: # Check for macOS and use the macOS-specific method + if platform.system() == 'Darwin': # Darwin is the system name for macOS + api_key = APIKeyManager.load_api_key_mac() + + # If no API key is found, prompt the user + if not api_key and parent: + parent.show_instructions() # Show the "How to Use" modal after setting the key + api_key = simpledialog.askstring("API Key", "Enter your OpenAI API Key:", parent=parent) + if api_key: + try: + if platform.system() == 'Darwin': + APIKeyManager.save_api_key_mac(api_key) + else: + APIKeyManager.save_api_key(api_key) + messagebox.showinfo("API Key Set", "The OpenAI API Key has been updated successfully.") + except Exception as e: + messagebox.showerror("Error", f"Failed to save API key: {str(e)}") + + return api_key + + @staticmethod + def change_api_key(parent): + """Change the API key.""" + new_key = simpledialog.askstring("API Key", "Enter new OpenAI API Key:", parent=parent) + if new_key: + success = APIKeyManager.save_api_key(new_key) + if success: + messagebox.showinfo("API Key Updated", "The OpenAI API Key has been updated successfully.") + return new_key + return None \ No newline at end of file diff --git a/utils/hotkey_manager.py b/utils/hotkey_manager.py new file mode 100644 index 0000000..aad20be --- /dev/null +++ b/utils/hotkey_manager.py @@ -0,0 +1,133 @@ +import tkinter as tk +from tkinter import ttk, messagebox +import keyboard + +class HotkeyManager: + """Class to handle hotkey operations.""" + + def __init__(self, app): + self.app = app + self.setup_hotkeys() + + def setup_hotkeys(self): + """Set up hotkeys based on settings.""" + try: + # Attempt to clear existing hotkeys + keyboard.unhook_all() # This should clear all hotkeys in some versions of the library. + except AttributeError: + pass # Ignore if the method isn't supported + + settings = self.app.load_settings() + + def parse_hotkey(combo): + return '+'.join(filter(None, combo)) + + keyboard.add_hotkey(parse_hotkey(settings["hotkeys"]["record_start_stop"]), lambda: self.hotkey_record_trigger()) + keyboard.add_hotkey(parse_hotkey(settings["hotkeys"]["stop_recording"]), lambda: self.hotkey_stop_trigger()) + keyboard.add_hotkey(parse_hotkey(settings["hotkeys"]["play_last_audio"]), lambda: self.hotkey_play_last_audio_trigger()) + + def hotkey_play_last_audio_trigger(self): + """Trigger playing the last audio.""" + if hasattr(self.app, 'last_audio_file'): + self.app.play_last_audio() + else: + self.app.play_sound('assets/no-last-audio.wav') + + def hotkey_stop_trigger(self): + """Trigger stopping the recording.""" + self.app.play_sound('assets/wrong-short.wav') + if self.app.recording: + self.app.stop_recording(auto_play=False) + self.app.recording = False + + def hotkey_record_trigger(self): + """Trigger recording or stop recording and submit.""" + if self.app.recording: + self.app.play_sound('assets/pop.wav') + self.app.submit_text() + else: + if not self.app.recording: + self.app.start_recording(play_confirm_sound=True) + else: + self.app.stop_recording(auto_play=True) + + @staticmethod + def hotkey_settings_dialog(app): + """Show the hotkey settings dialog.""" + settings = app.load_settings() + hotkey_window = tk.Toplevel(app) + hotkey_window.title("Hotkey Settings") + hotkey_window.grab_set() # Grab the focus on this toplevel window + + main_frame = ttk.Frame(hotkey_window, padding="10") + main_frame.grid(column=0, row=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + # Create dropdowns for each hotkey + keys = ["", "ctrl", "shift", "alt", "tab", "altgr"] + main_keys = list("abcdefghijklmnopqrstuvwxyz1234567890[];'#,./`") + \ + [f"f{i}" for i in range(1, 13)] # Add function keys F1 to F12 + + def create_hotkey_row(label_text, key_combo): + ttk.Label(main_frame, text=label_text).grid(row=create_hotkey_row.row, column=0, sticky=tk.W, pady=2) + + var1 = tk.StringVar(value=key_combo[0] if len(key_combo) > 0 else "") + var2 = tk.StringVar(value=key_combo[1] if len(key_combo) > 1 else "") + var3 = tk.StringVar(value=key_combo[2] if len(key_combo) > 2 else "") + + option_menu1 = ttk.OptionMenu(main_frame, var1, key_combo[0], *keys) + option_menu1.grid(row=create_hotkey_row.row, column=1, sticky=tk.W, pady=2) + + option_menu2 = ttk.OptionMenu(main_frame, var2, key_combo[1] if len(key_combo) > 1 else "", *keys) + option_menu2.grid(row=create_hotkey_row.row, column=2, sticky=tk.W, pady=2) + + option_menu3 = ttk.OptionMenu(main_frame, var3, key_combo[2] if len(key_combo) > 2 else "", *main_keys) + option_menu3.grid(row=create_hotkey_row.row, column=3, sticky=tk.W, pady=2) + + create_hotkey_row.row += 1 + return [var1, var2, var3] + + create_hotkey_row.row = 0 + + record_start_stop_vars = create_hotkey_row("Record Start/Stop:", settings["hotkeys"]["record_start_stop"]) + stop_recording_vars = create_hotkey_row("Stop Recording:", settings["hotkeys"]["stop_recording"]) + play_last_audio_vars = create_hotkey_row("Play Last Audio:", settings["hotkeys"]["play_last_audio"]) + + # Save Button + save_btn = ttk.Button(main_frame, text="Save", command=lambda: HotkeyManager.save_hotkey_settings(app, { + "record_start_stop": [record_start_stop_vars[0].get(), record_start_stop_vars[1].get(), record_start_stop_vars[2].get()], + "stop_recording": [stop_recording_vars[0].get(), stop_recording_vars[1].get(), stop_recording_vars[2].get()], + "play_last_audio": [play_last_audio_vars[0].get(), play_last_audio_vars[1].get(), play_last_audio_vars[2].get()] + })) + save_btn.grid(row=create_hotkey_row.row, column=1, sticky=tk.W + tk.E, pady=10) + + @staticmethod + def save_hotkey_settings(app, hotkeys): + """Save hotkey settings.""" + settings = app.load_settings() + settings["hotkeys"] = hotkeys + app.save_settings_to_JSON(settings) + app.hotkey_manager.setup_hotkeys() # Re-register the hotkeys with the new settings + messagebox.showinfo("Settings Updated", "Your hotkey settings have been saved successfully.") + + @staticmethod + def show_hotkey_instructions(app): + """Show hotkey instructions.""" + instruction_window = tk.Toplevel(app) + instruction_window.title("Hotkey Instructions") + instruction_window.geometry("400x300") # Width x Height + + instructions = """How to use Hotkeys +ctrl+shift+0 +This starts a recording, then converts to text and plays when you press this hotkey again. + +ctrl+shift+9 +If you are recording, you can press this hotkey to stop recording without playing + +ctrl+shift+8 +This replays the last audio clip played + + """ + tk.Label(instruction_window, text=instructions, justify=tk.LEFT, wraplength=380).pack(padx=10, pady=10) + + # Add a button to close the window + ttk.Button(instruction_window, text="Close", command=instruction_window.destroy).pack(pady=(10, 0)) \ No newline at end of file diff --git a/utils/resource_utils.py b/utils/resource_utils.py new file mode 100644 index 0000000..a068331 --- /dev/null +++ b/utils/resource_utils.py @@ -0,0 +1,30 @@ +import os +import sys +from audioplayer import AudioPlayer + +class ResourceUtils: + """Utility class for handling resources and audio playback.""" + + @staticmethod + def resource_path(relative_path): + """Get the absolute path to the resource, works for both development and PyInstaller environments.""" + try: + # When running in a PyInstaller bundle, use the '_MEIPASS' directory + base_path = sys._MEIPASS + except AttributeError: + # When running normally (not bundled), use the directory where the main script is located + base_path = os.path.dirname(os.path.abspath(sys.argv[0])) + + # Resolve the absolute path + abs_path = os.path.join(base_path, relative_path) + + # Debugging: Print the absolute path to check if it's correct + print(f"Resolved path for {relative_path}: {abs_path}") + + return abs_path + + @staticmethod + def play_sound(sound_file): + """Play a sound file.""" + player = AudioPlayer(ResourceUtils.resource_path(sound_file)) + player.play(block=True) \ No newline at end of file diff --git a/utils/text_to_mic.py b/utils/text_to_mic.py index 3257eab..93ed56a 100644 --- a/utils/text_to_mic.py +++ b/utils/text_to_mic.py @@ -6,7 +6,6 @@ import pyaudio import wave import webbrowser import json -import keyboard import sys from pystray import Icon as icon, MenuItem as item, Menu as menu @@ -16,8 +15,11 @@ from openai import OpenAI from dotenv import load_dotenv from pathlib import Path from pydub import AudioSegment -from audioplayer import AudioPlayer +# Import our refactored classes +from utils.api_key_manager import APIKeyManager +from utils.hotkey_manager import HotkeyManager +from utils.resource_utils import ResourceUtils # Modify the load environment variables to load from config/.env def load_env_file(): @@ -55,8 +57,8 @@ class TextToMic(tk.Tk): self.ensure_config_directory() load_env_file() - # Ensure API Key is loaded or prompted for before initializing GUI components - self.api_key = self.get_api_key() + # Get API key using APIKeyManager + self.api_key = APIKeyManager.get_api_key(self) if not self.api_key: messagebox.showinfo("API Key Needed", "Please provide your OpenAI API Key.") self.destroy() @@ -74,11 +76,10 @@ class TextToMic(tk.Tk): self.create_menu() self.initialize_gui() - self.setup_hotkeys() - - - + # Initialize our HotkeyManager + self.hotkey_manager = HotkeyManager(self) + def ensure_config_directory(self): """Ensure the config directory exists.""" config_dir = Path("config") @@ -105,14 +106,13 @@ class TextToMic(tk.Tk): self.menubar.add_cascade(label="Settings", menu=settings_menu) settings_menu.add_command(label="Change API Key", command=self.change_api_key) settings_menu.add_command(label="ChatGPT Manipulation", command=self.chat_gpt_settings) - settings_menu.add_command(label="Hotkey Settings", command=self.hotkey_settings) + settings_menu.add_command(label="Hotkey Settings", command=self.show_hotkey_settings) # Playback menu playback_menu = Menu(self.menubar, tearoff=0) self.menubar.add_cascade(label="Playback", menu=playback_menu) playback_menu.add_command(label="Play Last Audio", command=self.play_last_audio) - #playback_menu.add_command(label="Input Speech to Text", command=self.input_speech_to_text) #apply_ai input_menu = Menu(self.menubar, tearoff=0) @@ -127,136 +127,36 @@ class TextToMic(tk.Tk): help_menu.add_command(label="Terms of Use and Licence", command=self.show_terms_of_use) help_menu.add_command(label="Version", command=self.show_version) help_menu.add_command(label="Hotkey Instructions", command=self.show_hotkey_instructions) - + def show_hotkey_settings(self): + """Show the hotkey settings dialog.""" + HotkeyManager.hotkey_settings_dialog(self) - def hotkey_settings(self): - settings = self.load_settings() - hotkey_window = tk.Toplevel(self) - hotkey_window.title("Hotkey Settings") - hotkey_window.grab_set() # Grab the focus on this toplevel window + def show_hotkey_instructions(self): + """Show hotkey instructions.""" + HotkeyManager.show_hotkey_instructions(self) - main_frame = ttk.Frame(hotkey_window, padding="10") - main_frame.grid(column=0, row=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + def change_api_key(self): + """Change the API key using APIKeyManager.""" + new_key = APIKeyManager.change_api_key(self) + if new_key: + self.api_key = new_key + self.client = OpenAI(api_key=self.api_key) - # Create dropdowns for each hotkey - keys = ["", "ctrl", "shift", "alt", "tab", "altgr"] - main_keys = list("abcdefghijklmnopqrstuvwxyz1234567890[];'#,./`") + \ - [f"f{i}" for i in range(1, 13)] # Add function keys F1 to F12 - - def create_hotkey_row(label_text, key_combo): - ttk.Label(main_frame, text=label_text).grid(row=create_hotkey_row.row, column=0, sticky=tk.W, pady=2) - - var1 = tk.StringVar(value=key_combo[0] if len(key_combo) > 0 else "") - var2 = tk.StringVar(value=key_combo[1] if len(key_combo) > 1 else "") - var3 = tk.StringVar(value=key_combo[2] if len(key_combo) > 2 else "") - - option_menu1 = ttk.OptionMenu(main_frame, var1, key_combo[0], *keys) - option_menu1.grid(row=create_hotkey_row.row, column=1, sticky=tk.W, pady=2) - - option_menu2 = ttk.OptionMenu(main_frame, var2, key_combo[1] if len(key_combo) > 1 else "", *keys) - option_menu2.grid(row=create_hotkey_row.row, column=2, sticky=tk.W, pady=2) - - option_menu3 = ttk.OptionMenu(main_frame, var3, key_combo[2] if len(key_combo) > 2 else "", *main_keys) - option_menu3.grid(row=create_hotkey_row.row, column=3, sticky=tk.W, pady=2) - - create_hotkey_row.row += 1 - return [var1, var2, var3] - - create_hotkey_row.row = 0 - - record_start_stop_vars = create_hotkey_row("Record Start/Stop:", settings["hotkeys"]["record_start_stop"]) - stop_recording_vars = create_hotkey_row("Stop Recording:", settings["hotkeys"]["stop_recording"]) - play_last_audio_vars = create_hotkey_row("Play Last Audio:", settings["hotkeys"]["play_last_audio"]) - - # Save Button - save_btn = ttk.Button(main_frame, text="Save", command=lambda: self.save_hotkey_settings({ - "record_start_stop": [record_start_stop_vars[0].get(), record_start_stop_vars[1].get(), record_start_stop_vars[2].get()], - "stop_recording": [stop_recording_vars[0].get(), stop_recording_vars[1].get(), stop_recording_vars[2].get()], - "play_last_audio": [play_last_audio_vars[0].get(), play_last_audio_vars[1].get(), play_last_audio_vars[2].get()] - })) - save_btn.grid(row=create_hotkey_row.row, column=1, sticky=tk.W + tk.E, pady=10) - - - def save_hotkey_settings(self, hotkeys): - settings = self.load_settings() - settings["hotkeys"] = hotkeys - self.save_settings_to_JSON(settings) - self.setup_hotkeys() # Re-register the hotkeys with the new settings - messagebox.showinfo("Settings Updated", "Your hotkey settings have been saved successfully.") - - def setup_hotkeys(self): - try: - # Attempt to clear existing hotkeys - keyboard.unhook_all() # This should clear all hotkeys in some versions of the library. - except AttributeError: - pass # Ignore if the method isn't supported - - settings = self.load_settings() - - def parse_hotkey(combo): - return '+'.join(filter(None, combo)) - - keyboard.add_hotkey(parse_hotkey(settings["hotkeys"]["record_start_stop"]), lambda: self.hotkey_record_trigger()) - keyboard.add_hotkey(parse_hotkey(settings["hotkeys"]["stop_recording"]), lambda: self.hotkey_stop_trigger()) - keyboard.add_hotkey(parse_hotkey(settings["hotkeys"]["play_last_audio"]), lambda: self.hotkey_play_last_audio_trigger()) - - - def hotkey_play_last_audio_trigger(self): - if hasattr(self, 'last_audio_file'): - self.play_last_audio() + def get_audio_file_path(self, filename): + if platform.system() == 'Darwin': # Check if the OS is macOS + mac_path = APIKeyManager.get_app_support_path_mac() + return f"{mac_path}/{filename}" else: - self.play_sound('assets/no-last-audio.wav') - - - def hotkey_stop_trigger(self): - self.play_sound('assets/wrong-short.wav') - if self.recording: - self.stop_recording(auto_play=False) - self.recording=False - - # Sounds from https://mixkit.co/free-sound-effects/notification/ - def hotkey_record_trigger(self): - - if self.recording: - self.play_sound('assets/pop.wav') - self.submit_text() - else: - - if not self.recording: - self.start_recording(play_confirm_sound=True) - else: - self.stop_recording(auto_play=True) - - - - - + return Path(filename) # Default to current directory for non-macOS systems def play_sound(self, sound_file): - player = AudioPlayer(self.resource_path(sound_file)) - player.play(block=True) + """Play a sound file using ResourceUtils.""" + ResourceUtils.play_sound(sound_file) def resource_path(self, relative_path): - """Get the absolute path to the resource, works for both development and PyInstaller environments.""" - - try: - # When running in a PyInstaller bundle, use the '_MEIPASS' directory - base_path = sys._MEIPASS - except AttributeError: - # When running normally (not bundled), use the directory where the main script is located - base_path = os.path.dirname(os.path.abspath(sys.argv[0])) - - # Resolve the absolute path - abs_path = os.path.join(base_path, relative_path) - - # Debugging: Print the absolute path to check if it's correct - print(f"Resolved path for {relative_path}: {abs_path}") - - return abs_path - - - + """Get the resource path using ResourceUtils.""" + return ResourceUtils.resource_path(relative_path) def initialize_gui(self): @@ -449,27 +349,6 @@ class TextToMic(tk.Tk): - def show_hotkey_instructions(self): - instruction_window = tk.Toplevel(self) - instruction_window.title("Hotkey Instructions") - instruction_window.geometry("400x300") # Width x Height - - instructions = """How to use Hotkeys -ctrl+shift+0 -This starts a recording, then converts to text and plays when you press this hotkey again. - -ctrl+shift+9 -If you are recording, you can press this hotkey to stop recording without playing - -ctrl+shift+8 -This replays the last audio clip played - - """ - tk.Label(instruction_window, text=instructions, justify=tk.LEFT, wraplength=380).pack(padx=10, pady=10) - - # Add a button to close the window - ttk.Button(instruction_window, text="Close", command=instruction_window.destroy).pack(pady=(10, 0)) - def show_instructions(self): instruction_window = tk.Toplevel(self) instruction_window.title("How to Use") @@ -651,8 +530,7 @@ Please also make sure you read the Terms of use and licence statement before usi def get_audio_file_path(self, filename): if platform.system() == 'Darwin': # Check if the OS is macOS - mac_path = self.get_app_support_path_mac() - #return self.get_app_support_path_mac() / filename + mac_path = APIKeyManager.get_app_support_path_mac() return f"{mac_path}/{filename}" else: return Path(filename) # Default to current directory for non-macOS systems @@ -842,195 +720,6 @@ Please also make sure you read the Terms of use and licence statement before usi p.terminate() - def change_api_key(self): - new_key = simpledialog.askstring("API Key", "Enter new OpenAI API Key:", parent=self) - if new_key: - self.save_api_key(new_key) - self.api_key = new_key - self.client = OpenAI(api_key=self.api_key) - messagebox.showinfo("API Key Updated", "The OpenAI API Key has been updated successfully.") - - - def get_device_info(self, device_index): - p = pyaudio.PyAudio() - try: - device_info = p.get_device_info_by_index(device_index) - return device_info - finally: - p.terminate() - - def toggle_recording(self, auto_play=False): - if not self.recording: - self.start_recording() - else: - self.stop_recording(auto_play) - - def stop_recording_btn_change(self, btn_text): - self.record_button.config(text=btn_text) - - - def start_recording(self, play_confirm_sound=False): - - input_device_index = self.input_device_index.get() # Assuming input_device_index is a StringVar - input_device_id = self.available_input_devices.get(input_device_index) - - if input_device_id is None: - if play_confirm_sound: - self.play_sound('assets/please-select-input.wav') - else: - messagebox.showerror("Error", "Selected audio device is not available.") - return - - device_info = self.get_device_info(input_device_id) - sample_rate = int(device_info['defaultSampleRate']) - - print(f"Device info: {device_info}") - - if sample_rate is None: - sample_rate = 44100 - - #Record to GUI selected device ID - #device_id = None if self.input_device_index.get() == "Default" else input_devices[self.input_device_index.get()] - - if input_device_id is None: - messagebox.showerror("Error", "Selected audio device is not available.") - return - - try: - self.recording = True - self.record_button.config(text="Stop and Insert", style='Recording.TButton') - self.submit_button.config(text="Stop and Play", style='Recording.TButton') - - self.frames = [] - - self.p = pyaudio.PyAudio() - self.stream = self.p.open(format=pyaudio.paInt16, channels=1, rate=sample_rate, input=True, frames_per_buffer=1024, input_device_index=input_device_id) - - if play_confirm_sound: - self.play_sound('assets/pop.wav') - - def record(): - while self.recording: - data = self.stream.read(1024, exception_on_overflow=False) - self.frames.append(data) - - self.record_thread = threading.Thread(target=record) - self.record_thread.start() - - except Exception as e: - messagebox.showerror("Recording Error", f"Failed to record audio: {str(e)}") - self.stop_recording(True) - - def stop_recording(self, cancel_save=False, auto_play=False): - self.recording = False - if self.record_thread: - self.record_thread.join() - - if self.stream: - self.stream.stop_stream() - self.stream.close() - - if self.p: - self.p.terminate() - - if cancel_save==False: - self.save_recording(auto_play=auto_play) - - self.record_button.config(text="Record Mic", style='TButton') # Revert to default style - self.submit_button.config(text="Play", style='Green.TButton') # Revert to default style - - - def save_recording(self, auto_play = False): - file_path = "output.wav" - wf = wave.open(file_path, 'wb') - wf.setnchannels(1) - wf.setsampwidth(self.p.get_sample_size(pyaudio.paInt16)) - wf.setframerate(44100) - wf.writeframes(b''.join(self.frames)) - wf.close() - print("Recording saved.") - - self.after(0, self.transcribe_audio, file_path, auto_play) - - - - - def transcribe_audio(self, file_path, auto_play = False): - try: - with open(str(file_path), "rb") as audio_file: - transcription = self.client.audio.transcriptions.create( - file=audio_file, - model="whisper-1", - response_format="verbose_json" - ) - - settings = self.load_settings() - - if settings["chat_gpt_completion"] and settings["auto_apply_ai_to_recording"]: - auto_apply_ai = settings["auto_apply_ai_to_recording"] - else: - auto_apply_ai = False - - print(f"auto_apply_ai: {auto_apply_ai}") - - if auto_apply_ai: - print("applying ai") - play_text = self.apply_ai(transcription.text) - else: - print("outputting without ai") - #This prevents issues with trying to upload TK after thread operations - #whcih can cause crashes with no error displayed - self.text_input.delete("1.0", tk.END) # Clear existing text - self.text_input.insert("1.0", transcription.text) # Insert new text - play_text = transcription.text - - if auto_play: - #self.submit_text(play_text = playtext)# - print(f"Triggering auto play with: {play_text} ") - self.submit_text_helper(play_text = play_text) - # TODO: PLAY THE TEXT IMMEDIATELY - - print("Transcription Complete: The audio has been transcribed and the text has been placed in the input area.") - #messagebox.showinfo("Transcription Complete", "The audio has been transcribed and the text has been placed in the input area.") - - except Exception as e: - print(f"Transcription error: An error occurred during transcription: {str(e)}") - - - def load_settings(self): - settings_file = self.get_settings_file_path("settings.json") - try: - with open(settings_file, "r") as f: - settings = json.load(f) - except FileNotFoundError: - # Default settings - settings = { - "chat_gpt_completion": False, - "model": self.default_model, - "prompt": "", - "auto_apply_ai_to_recording": False, - "hotkeys": { - "record_start_stop": ["ctrl", "shift", "0"], - "stop_recording": ["ctrl", "shift", "9"], - "play_last_audio": ["ctrl", "shift", "8"] - } - } - self.save_settings_to_JSON(settings) - return settings - - def save_settings_to_JSON(self, settings): - settings_file = self.get_settings_file_path("settings.json") - - with open(settings_file, "w") as f: - json.dump(settings, f) - - def get_settings_file_path(self, filename): - if platform.system() == 'Darwin': # Check if the OS is macOS - mac_path = self.get_app_support_path_mac() - return f"{mac_path}/{filename}" - else: - return filename # Default to current directory for non-macOS systems - def chat_gpt_settings(self): settings = self.load_settings() settings_window = tk.Toplevel(self) @@ -1351,4 +1040,171 @@ Please also make sure you read the Terms of use and licence statement before usi self.save_presets() self.refresh_presets_display() + def get_device_info(self, device_index): + p = pyaudio.PyAudio() + try: + device_info = p.get_device_info_by_index(device_index) + return device_info + finally: + p.terminate() + + def toggle_recording(self, auto_play=False): + if not self.recording: + self.start_recording() + else: + self.stop_recording(auto_play) + + def stop_recording_btn_change(self, btn_text): + self.record_button.config(text=btn_text) + + def start_recording(self, play_confirm_sound=False): + input_device_index = self.input_device_index.get() # Assuming input_device_index is a StringVar + input_device_id = self.available_input_devices.get(input_device_index) + + if input_device_id is None: + if play_confirm_sound: + self.play_sound('assets/please-select-input.wav') + else: + messagebox.showerror("Error", "Selected audio device is not available.") + return + + device_info = self.get_device_info(input_device_id) + sample_rate = int(device_info['defaultSampleRate']) + + print(f"Device info: {device_info}") + + if sample_rate is None: + sample_rate = 44100 + + if input_device_id is None: + messagebox.showerror("Error", "Selected audio device is not available.") + return + + try: + self.recording = True + self.record_button.config(text="Stop and Insert", style='Recording.TButton') + self.submit_button.config(text="Stop and Play", style='Recording.TButton') + + self.frames = [] + + self.p = pyaudio.PyAudio() + self.stream = self.p.open(format=pyaudio.paInt16, channels=1, rate=sample_rate, input=True, frames_per_buffer=1024, input_device_index=input_device_id) + + if play_confirm_sound: + self.play_sound('assets/pop.wav') + + def record(): + while self.recording: + data = self.stream.read(1024, exception_on_overflow=False) + self.frames.append(data) + + self.record_thread = threading.Thread(target=record) + self.record_thread.start() + + except Exception as e: + messagebox.showerror("Recording Error", f"Failed to record audio: {str(e)}") + self.stop_recording(True) + + def stop_recording(self, cancel_save=False, auto_play=False): + self.recording = False + if hasattr(self, 'record_thread') and self.record_thread: + self.record_thread.join() + + if hasattr(self, 'stream') and self.stream: + self.stream.stop_stream() + self.stream.close() + + if hasattr(self, 'p') and self.p: + self.p.terminate() + + if cancel_save==False: + self.save_recording(auto_play=auto_play) + + self.record_button.config(text="Record Mic", style='TButton') # Revert to default style + self.submit_button.config(text="Play", style='Green.TButton') # Revert to default style + + def save_recording(self, auto_play = False): + file_path = "output.wav" + wf = wave.open(file_path, 'wb') + wf.setnchannels(1) + wf.setsampwidth(self.p.get_sample_size(pyaudio.paInt16)) + wf.setframerate(44100) + wf.writeframes(b''.join(self.frames)) + wf.close() + print("Recording saved.") + + self.after(0, self.transcribe_audio, file_path, auto_play) + + def transcribe_audio(self, file_path, auto_play = False): + try: + with open(str(file_path), "rb") as audio_file: + transcription = self.client.audio.transcriptions.create( + file=audio_file, + model="whisper-1", + response_format="verbose_json" + ) + + settings = self.load_settings() + + if settings["chat_gpt_completion"] and settings["auto_apply_ai_to_recording"]: + auto_apply_ai = settings["auto_apply_ai_to_recording"] + else: + auto_apply_ai = False + + print(f"auto_apply_ai: {auto_apply_ai}") + + if auto_apply_ai: + print("applying ai") + play_text = self.apply_ai(transcription.text) + else: + print("outputting without ai") + #This prevents issues with trying to upload TK after thread operations + #whcih can cause crashes with no error displayed + self.text_input.delete("1.0", tk.END) # Clear existing text + self.text_input.insert("1.0", transcription.text) # Insert new text + play_text = transcription.text + + if auto_play: + print(f"Triggering auto play with: {play_text} ") + self.submit_text_helper(play_text = play_text) + + print("Transcription Complete: The audio has been transcribed and the text has been placed in the input area.") + + except Exception as e: + print(f"Transcription error: An error occurred during transcription: {str(e)}") + + def load_settings(self): + settings_file = self.get_settings_file_path("settings.json") + try: + with open(settings_file, "r") as f: + settings = json.load(f) + except FileNotFoundError: + # Default settings + settings = { + "chat_gpt_completion": False, + "model": self.default_model, + "prompt": "", + "auto_apply_ai_to_recording": False, + "hotkeys": { + "record_start_stop": ["ctrl", "shift", "0"], + "stop_recording": ["ctrl", "shift", "9"], + "play_last_audio": ["ctrl", "shift", "8"] + } + } + self.save_settings_to_JSON(settings) + return settings + + def save_settings_to_JSON(self, settings): + settings_file = self.get_settings_file_path("settings.json") + + with open(settings_file, "w") as f: + json.dump(settings, f) + + def get_settings_file_path(self, filename): + if platform.system() == 'Darwin': # Check if the OS is macOS + mac_path = APIKeyManager.get_app_support_path_mac() + return f"{mac_path}/{filename}" + else: + return filename # Default to current directory for non-macOS systems +